diff --git a/.githooks/_/pre-commit b/.githooks/_/pre-commit new file mode 100755 index 00000000..c4a7cba0 --- /dev/null +++ b/.githooks/_/pre-commit @@ -0,0 +1,12 @@ +#!/bin/sh + +if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then + echo "[INFO] SKIP_SIMPLE_GIT_HOOKS is set to 1, skipping hook." + exit 0 +fi + +if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then + . "$SIMPLE_GIT_HOOKS_RC" +fi + +cd packages/web && deno task lint:fix && deno task format \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e0d41bd6..d3fe3f05 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -46,4 +46,5 @@ Check all that apply. If an item doesn't apply to your PR, you can leave it unch - [ ] Code follows project style guidelines - [ ] Documentation has been updated or added - [ ] Tests have been added or updated -- [ ] All i18n translation labels have bee added +- [ ] All i18n translation labels have been added (read + CONTRIBUTING_I18N_DEVELOPER_GUIDE.md for more details) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9a230d6..3629d269 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,22 +29,16 @@ jobs: with: path: | ~/.cache/deno - ./deno.lock - key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }} + packages/web/deno.lock + key: ${{ runner.os }}-deno-${{ hashFiles('packages/web/deno.lock') }} restore-keys: | ${{ runner.os }}-deno- - - name: Cache Dependencies - run: deno cache src/index.tsx - - name: Run linter - run: deno task lint + run: deno lint - name: Check formatter - run: deno task format --check - - - name: Run tests - run: deno task test + run: deno fmt --check - name: Build Package run: deno task build diff --git a/.github/workflows/crowdin-upload-sources.yml b/.github/workflows/crowdin-upload-sources.yml index 55360f17..249c7e73 100644 --- a/.github/workflows/crowdin-upload-sources.yml +++ b/.github/workflows/crowdin-upload-sources.yml @@ -2,10 +2,10 @@ name: Crowdin Upload Sources Action on: push: - # Monitor all .json files within the /src/i18n/locales/en/ directory. + # Monitor all .json files within the /i18n/locales/en/ directory. # This ensures the workflow triggers if any the English namespace files are modified on the main branch. paths: - - "/src/i18n/locales/en/**/*.json" + - "/i18n/locales/en/*.json" branches: [main] workflow_dispatch: # Allow manual triggering diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml new file mode 100644 index 00000000..e03b29e7 --- /dev/null +++ b/.github/workflows/js-ci.yml @@ -0,0 +1,37 @@ +name: Pull Request + +on: + push: + paths: + - "packages/core" + - "packages/transport-deno" + - "packages/transport-http" + - "packages/transport-web-bluetooth" + - "packages/transport-web-serial" + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Check formatting + run: deno fmt --check + + - name: Check types + run: deno lint + + - name: Publish to JSR + run: npx jsr publish diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ad1dfc62..9846c80c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -21,22 +21,26 @@ jobs: deno-version: v2.x - name: Install Dependencies + working-directory: packages/web run: deno install - name: Run tests + working-directory: packages/web run: deno task test - name: Build Package + working-directory: packages/web run: deno task build - name: Package Output + working-directory: packages/web run: deno task package - name: Archive compressed build uses: actions/upload-artifact@v4 with: name: build - path: dist/build.tar + path: packages/web/dist/build.tar - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 4ed16f73..459ca916 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,10 +1,13 @@ name: Pull Request CI -on: - pull_request: +on: pull_request + +permissions: + contents: write + packages: write jobs: - build: + build-and-package: runs-on: ubuntu-latest steps: - name: Checkout code @@ -15,39 +18,29 @@ jobs: with: deno-version: v2.x + - name: Install Dependencies + # Commands will run from 'packages/web' + working-directory: packages/web + run: deno install + - name: Cache Deno dependencies uses: actions/cache@v4 with: path: | ~/.cache/deno - ./deno.lock - key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }} + packages/web/deno.lock + key: ${{ runner.os }}-deno-${{ hashFiles('packages/web/deno.lock') }} restore-keys: | ${{ runner.os }}-deno- - - name: Install Dependencies - run: deno install - - - name: Cache Dependencies - run: deno cache src/index.tsx - - name: Run linter - run: deno task lint + working-directory: packages/web + run: deno lint - name: Check formatter - run: deno task format --check - - - name: Run tests - run: deno task test + working-directory: packages/web + run: deno fmt --check - name: Build Package - run: deno task build - - - name: Compress build - run: deno task package - - - name: Archive compressed build - uses: actions/upload-artifact@v4 - with: - name: build - path: dist/build.tar + working-directory: packages/web + run: deno task --filter web build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4bc0eb7..887dc64c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,26 +21,30 @@ jobs: deno-version: v2.x - name: Install Dependencies + working-directory: packages/web run: deno install - name: Run tests + working-directory: packages/web run: deno task test - name: Build Package + working-directory: packages/web run: deno task build - name: Package Output + working-directory: packages/web run: deno task package - name: Archive compressed build uses: actions/upload-artifact@v4 with: name: build - path: dist/build.tar + path: packages/web/dist/build.tar - name: Attach build.tar to release run: | - gh release upload ${{ github.event.release.tag_name }} dist/build.tar + gh release upload ${{ github.event.release.tag_name }} packages/web/dist/build.tar env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 7e424cce..c6da962b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ stats.html .vite dev-dist __screenshots__* -package-lock.json +npm/ +*.diff diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e11f647b..15b1f6c5 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["bradlc.vscode-tailwindcss", "biomejs.biome"] + "recommendations": ["bradlc.vscode-tailwindcss", "denoland.vscode-deno"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 9045fc0f..04d2f6e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,8 @@ "deno.enable": true, "deno.suggest.imports.autoDiscover": true, "editor.formatOnSave": true, - "editor.defaultFormatter": "denoland.vscode-deno" + "editor.defaultFormatter": "denoland.vscode-deno", + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + } } diff --git a/README.md b/README.md index b64adf57..442a7642 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,106 @@ -# Meshtastic Web +# Meshtastic Web Monorepo - - -[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/web/ci.yml?branch=main&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/web/actions/workflows/ci.yml) +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/web/ci.yml?branch=main&label=Web%20CI&logo=github&color=yellow)](https://github.com/meshtastic/web/actions/workflows/ci.yml) +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=JS%20CI&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml) [![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/web)](https://cla-assistant.io/meshtastic/web) [![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) [![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) ## Overview -Official [Meshtastic](https://meshtastic.org) web interface, that can be hosted -or served from a node - -**[Hosted version](https://client.meshtastic.org)** - -## Stats - -![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image") - -## Progress Web App Support (PWA) - -Meshtastic Web Client now includes Progressive Web App (PWA) functionality, -allowing users to: +This monorepo consolidates the official [Meshtastic](https://meshtastic.org) web +interface and its supporting JavaScript libraries. It aims to provide a unified +development experience for interacting with Meshtastic devices. -- Install the app on desktop and mobile devices -- Access the interface offline -- Receive updates automatically -- Experience faster load times with caching +### Projects within this Monorepo (`packages/`) -To install as a PWA: +All projects are located within the `packages/` directory: -- On desktop: Look for the install icon in your browser's address bar -- On mobile: Use "Add to Home Screen" option in your browser menu +- **`packages/web` (Meshtastic Web Client):** The official web interface, + designed to be hosted or served directly from a Meshtastic node. + - **[Hosted version](https://client.meshtastic.org)** +- **`packages/core`:** Core functionality for Meshtastic JS. +- **`packages/transport-deno`:** TCP Transport for the Deno runtime. +- **`packages/transport-http`:** HTTP Transport. +- **`packages/transport-web-bluetooth`:** Web Bluetooth Transport. +- **`packages/transport-web-serial`:** Web Serial Transport. -PWA functionality works with both the hosted version and self-hosted instances. +All `Meshtastic JS` packages (core and transports) are published to +[JSR](https://jsr.io/@meshtastic). -## Self-host +--- -The client can be self hosted using the precompiled container images with an OCI -compatible runtime such as [Docker](https://www.docker.com/) or -[Podman](https://podman.io/). The base image used is -[Nginx 1.27](https://hub.docker.com/_/nginx) - -```bash -# With Docker -docker run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web - -#With Podman -podman run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web -``` - -## Release Schedule +## Stats -Our release process follows these guidelines: +| Project | Repobeats | +| :-------------------- | :-------------------------------------------------------------------------------------------------------------------- | +| Meshtastic Web Client | ![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image") | +| Meshtastic JS | ![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") | -- **Versioning:** We use Semantic Versioning (`Major.Minor.Patch`). -- **Stable Releases:** Published around the beginning of each month (e.g., - `v2.3.4`). -- **Pre-releases:** A pre-release is typically issued mid-month for testing and - early adoption. -- **Nightly Builds:** An experimental Docker image containing the latest - cutting-edge features and fixes is automatically built nightly from the `main` - branch. +--- -### Nightly Builds +## Tech Stack -```bash -# With Docker -docker run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web:nightly -#With Podman -podman run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web:nightly -``` +This monorepo leverages the following technologies: -> [!WARNING] -> -> - Nightly builds represent the latest development state and may contain -> breaking changes -> - These builds undergo automated testing but may be less stable than tagged -> release versions -> - Not recommended for production environments unless you are actively testing -> new features -> - No guarantee of backward compatibility between nightly builds +- **Runtime:** Deno +- **Web Client:** React.js +- **Styling:** Tailwind CSS +- **Bundling:** Vite +- **Language:** TypeScript +- **Testing:** Vitest, React Testing Library -#### Version Information +--- -Each nightly build is tagged with: +## Getting Started -- The nightly tag for the latest build -- A specific SHA for build reproducibility +### Prerequisites -### Feedback +You'll need to have [Deno](https://deno.com/) installed to work with this +monorepo. Follow the installation instructions on their home page. -If you encounter any issues with nightly builds, please report them in our -[issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps -improve the stability of future releases +### Development Setup -## Development & Building +1. **Clone the repository:** + ```bash + git clone [https://github.com/meshtastic/meshtastic-web.git](https://github.com/meshtastic/meshtastic-web.git) + cd meshtastic-web + ``` +2. **Install dependencies for all packages:** + ```bash + deno task install:all + ``` + This command installs all necessary dependencies for all packages within the + monorepo. -You'll need to download the package manager used with this repo. You can install -it by visiting [deno.com](https://deno.com/) and following the installation -instructions listed on the home page. +### Running Projects -### Development +#### Meshtastic Web Client -Install the dependencies. +To start the development server for the web client: ```bash -deno i +deno task --filter web dev ``` -Start the development server: - -```bash -deno task dev -``` +This will typically run the web client on http://localhost:3000 and requires a +Chromium browser -### Building and Packaging +## Meshtastic JS Packages -Build the project: +While the js packages are primarily libraries, you can run their tests or +specific development scripts if defined within their deno.json files. For +example, to run tests for a specific package: ```bash -deno task build +deno task --filter core test ``` -GZip the output: +### Feedback -```bash -deno task package -``` +If you encounter any issues with nightly builds, please report them in our +[issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps +improve the stability of future releases ### Why Deno? diff --git a/deno.json b/deno.json index 5317549d..dc347e90 100644 --- a/deno.json +++ b/deno.json @@ -1,48 +1,34 @@ { + "workspace": [ + "./packages/web", + "./packages/core", + "./packages/transport-deno", + "./packages/transport-http", + "./packages/transport-web-bluetooth", + "./packages/transport-web-serial" + ], "imports": { - "@app/": "./src/", - "@pages/": "./src/pages/", - "@components/": "./src/components/", - "@core/": "./src/core/", - "@layouts/": "./src/layouts/", - "@std/path": "jsr:@std/path@^1.1.0" - }, - "compilerOptions": { - "lib": [ - "DOM", - "DOM.Iterable", - "ESNext", - "deno.window", - "deno.ns" - ], - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "strictNullChecks": true, - "types": [ - "vite/client", - "node", - "@types/web-bluetooth", - "@types/w3c-web-serial" - ], - "strictPropertyInitialization": false + "@bufbuild/protobuf": "npm:@bufbuild/protobuf@^2.2.3", + "@meshtastic/protobufs": "jsr:@meshtastic/protobufs@^2.7.0", + "@types/node": "npm:@types/node@^22.13.10", + "ste-simple-events": "npm:ste-simple-events@^3.0.11", + "tslog": "npm:tslog@^4.9.3" }, - "fmt": { + "nodeModulesDir": "auto", + "lint": { "exclude": [ - "src/*.gen.ts", + "*routeTree.gen.ts", "*.test.ts", "*.test.tsx" - ] + ], + "report": "pretty" }, - "lint": { + "fmt": { "exclude": [ - "src/*.gen.ts", + "*routeTree.gen.ts", "*.test.ts", "*.test.tsx" - ], - "report": "pretty" + ] }, "unstable": [ "sloppy-imports" diff --git a/deno.lock b/deno.lock index 8cd8634b..b70bc65b 100644 --- a/deno.lock +++ b/deno.lock @@ -1,97 +1,118 @@ { "version": "5", "specifiers": { - "jsr:@std/path@*": "1.0.6", - "jsr:@std/path@^1.1.0": "1.1.0", - "npm:@bufbuild/protobuf@^2.2.5": "2.2.5", - "npm:@jsr/meshtastic__core@2.6.2": "2.6.2", - "npm:@noble/curves@^1.9.0": "1.9.0", - "npm:@radix-ui/react-accordion@^1.2.8": "1.2.8_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-checkbox@^1.2.3": "1.2.3_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-dialog@^1.1.11": "1.1.11_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-dropdown-menu@^2.1.12": "2.1.12_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-label@^2.1.4": "2.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-menubar@^1.1.12": "1.1.12_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-popover@^1.1.11": "1.1.11_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-scroll-area@^1.2.6": "1.2.6_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-select@^2.2.2": "2.2.2_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-separator@^1.1.4": "1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-slider@^1.3.2": "1.3.2_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-switch@^1.2.2": "1.2.2_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-tabs@^1.1.9": "1.1.9_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-toast@^1.2.11": "1.2.11_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-toggle-group@^1.1.9": "1.1.10_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@radix-ui/react-tooltip@^1.2.4": "1.2.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@tailwindcss/postcss@^4.1.5": "4.1.5", - "npm:@tanstack/react-router-devtools@^1.120.16": "1.120.16_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0_solid-js@1.9.7__seroval@1.3.2", - "npm:@tanstack/react-router@^1.120.15": "1.120.15_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@tanstack/router-devtools@^1.120.15": "1.120.15_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@tanstack/router-plugin@^1.120.15": "1.120.15_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@babel+core@7.27.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+node@22.15.3", + "jsr:@meshtastic/protobufs@^2.7.0": "2.7.0", + "jsr:@std/internal@^1.0.9": "1.0.9", + "jsr:@std/path@^1.1.0": "1.1.1", + "npm:@bufbuild/protobuf@^2.2.3": "2.6.0", + "npm:@bufbuild/protobuf@^2.6.0": "2.6.0", + "npm:@hookform/resolvers@^5.1.1": "5.1.1_react-hook-form@7.60.0__react@19.1.0_react@19.1.0", + "npm:@jsr/meshtastic__core@2.6.4": "2.6.4", + "npm:@jsr/meshtastic__js@2.6.0-0": "2.6.0-0", + "npm:@jsr/meshtastic__transport-http@*": "0.2.1", + "npm:@jsr/meshtastic__transport-web-bluetooth@*": "0.1.2", + "npm:@jsr/meshtastic__transport-web-serial@*": "0.2.1", + "npm:@noble/curves@^1.9.2": "1.9.2", + "npm:@radix-ui/react-accordion@^1.2.11": "1.2.11_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-checkbox@^1.3.2": "1.3.2_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-dialog@^1.1.14": "1.1.14_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-dropdown-menu@^2.1.15": "2.1.15_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-label@^2.1.7": "2.1.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-menubar@^1.1.15": "1.1.15_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-popover@^1.1.14": "1.1.14_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-scroll-area@^1.2.9": "1.2.9_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-select@^2.2.5": "2.2.5_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-separator@^1.1.7": "1.1.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-slider@^1.3.5": "1.3.5_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-switch@^1.2.5": "1.2.5_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-tabs@^1.1.12": "1.1.12_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-toast@^1.2.14": "1.2.14_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-toggle-group@^1.1.10": "1.1.10_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@radix-ui/react-tooltip@^1.2.7": "1.2.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@tailwindcss/postcss@^4.1.11": "4.1.11", + "npm:@tanstack/react-router-devtools@^1.125.4": "1.125.6_@tanstack+react-router@1.125.6__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@tanstack/react-router@^1.125.4": "1.125.6_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@tanstack/router-cli@^1.125.4": "1.125.4", + "npm:@tanstack/router-devtools@^1.125.4": "1.125.6_@tanstack+react-router@1.125.6__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@tanstack/router-plugin@^1.125.5": "1.125.6_@tanstack+react-router@1.125.6__react@19.1.0__react-dom@19.1.0___react@19.1.0_vite@7.0.2__@types+node@22.16.0__picomatch@4.0.2_@babel+core@7.28.0_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+node@22.16.0", "npm:@testing-library/jest-dom@^6.6.3": "6.6.3", - "npm:@testing-library/react@^16.3.0": "16.3.0_@testing-library+dom@10.4.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:@testing-library/react@^16.3.0": "16.3.0_@testing-library+dom@10.4.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0", "npm:@testing-library/user-event@^14.6.1": "14.6.1_@testing-library+dom@10.4.0", "npm:@turf/turf@^7.2.0": "7.2.0", - "npm:@types/chrome@^0.0.318": "0.0.318", + "npm:@types/chrome@^0.0.329": "0.0.329", "npm:@types/js-cookie@^3.0.6": "3.0.6", - "npm:@types/node@^22.15.3": "22.15.3", - "npm:@types/react-dom@^19.1.3": "19.1.3_@types+react@19.1.2", - "npm:@types/react@^19.1.2": "19.1.2", - "npm:@types/serviceworker@^0.0.133": "0.0.133", + "npm:@types/node@^22.13.10": "22.16.0", + "npm:@types/node@^24.0.10": "24.0.10", + "npm:@types/react-dom@^19.1.6": "19.1.6_@types+react@19.1.8", + "npm:@types/react@^19.1.8": "19.1.8", + "npm:@types/serviceworker@^0.0.140": "0.0.140", + "npm:@types/w3c-web-serial@*": "1.0.8", + "npm:@types/w3c-web-serial@^1.0.7": "1.0.8", "npm:@types/w3c-web-serial@^1.0.8": "1.0.8", + "npm:@types/web-bluetooth@*": "0.0.20", + "npm:@types/web-bluetooth@^0.0.20": "0.0.20", "npm:@types/web-bluetooth@^0.0.21": "0.0.21", - "npm:@vitejs/plugin-react@^4.4.1": "4.4.1_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@babel+core@7.27.1_@types+node@22.15.3", - "npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.3", + "npm:@vitejs/plugin-react@^4.6.0": "4.6.0_vite@7.0.2__@types+node@22.16.0__picomatch@4.0.2_@babel+core@7.28.0_@types+node@22.16.0", + "npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6", "npm:base64-js@^1.5.1": "1.5.1", "npm:class-variance-authority@~0.7.1": "0.7.1", "npm:clsx@^2.1.1": "2.1.1", - "npm:cmdk@^1.1.1": "1.1.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2", + "npm:cmdk@^1.1.1": "1.1.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8", + "npm:crc@^4.3.2": "4.3.2", "npm:crypto-random-string@5": "5.0.0", "npm:gzipper@^8.2.1": "8.2.1", - "npm:happy-dom@^17.4.6": "17.4.6", - "npm:i18next-browser-languagedetector@^8.1.0": "8.1.0", + "npm:happy-dom@^18.0.1": "18.0.1", + "npm:i18next-browser-languagedetector@^8.2.0": "8.2.0", "npm:i18next-http-backend@^3.0.2": "3.0.2", - "npm:i18next@^25.2.0": "25.2.0_typescript@5.8.3", - "npm:idb-keyval@^6.2.1": "6.2.1", + "npm:i18next@^25.3.1": "25.3.1_typescript@5.8.3", + "npm:idb-keyval@^6.2.2": "6.2.2", "npm:immer@^10.1.1": "10.1.1", "npm:js-cookie@^3.0.5": "3.0.5", - "npm:lucide-react@0.507": "0.507.0_react@19.1.0", - "npm:maplibre-gl@5.4.0": "5.4.0", - "npm:postcss@^8.5.3": "8.5.3", + "npm:lucide-react@0.525": "0.525.0_react@19.1.0", + "npm:maplibre-gl@5.6.1": "5.6.1", + "npm:postcss@^8.5.6": "8.5.6", "npm:react-dom@^19.1.0": "19.1.0_react@19.1.0", "npm:react-error-boundary@6": "6.0.0_react@19.1.0", - "npm:react-hook-form@^7.56.2": "7.56.2_react@19.1.0", - "npm:react-i18next@^15.5.1": "15.5.1_i18next@25.2.0__typescript@5.8.3_react@19.1.0_typescript@5.8.3", - "npm:react-map-gl@8.0.4": "8.0.4_maplibre-gl@5.4.0_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "npm:react-hook-form@^7.60.0": "7.60.0_react@19.1.0", + "npm:react-i18next@^15.6.0": "15.6.0_i18next@25.3.1__typescript@5.8.3_react@19.1.0_typescript@5.8.3", + "npm:react-map-gl@8.0.4": "8.0.4_maplibre-gl@5.6.1_react@19.1.0_react-dom@19.1.0__react@19.1.0", "npm:react-qrcode-logo@3": "3.0.0_react@19.1.0_react-dom@19.1.0__react@19.1.0", "npm:react@^19.1.0": "19.1.0", "npm:rfc4648@^1.5.4": "1.5.4", "npm:simple-git-hooks@^2.13.0": "2.13.0", - "npm:tailwind-merge@^3.2.0": "3.2.0", - "npm:tailwindcss-animate@^1.0.7": "1.0.7_tailwindcss@4.1.5", - "npm:tailwindcss@^4.1.5": "4.1.5", + "npm:ste-simple-events@^3.0.11": "3.0.11", + "npm:tailwind-merge@^3.3.1": "3.3.1", + "npm:tailwindcss-animate@^1.0.7": "1.0.7_tailwindcss@4.1.11", + "npm:tailwindcss@^4.1.11": "4.1.11", "npm:tar@^7.4.3": "7.4.3", "npm:testing-library@^0.0.2": "0.0.2_@angular+common@6.1.10__@angular+core@6.1.10___rxjs@6.6.7___zone.js@0.8.29__rxjs@6.6.7_@angular+core@6.1.10__rxjs@6.6.7__zone.js@0.8.29", + "npm:tslog@^4.9.3": "4.9.3", "npm:typescript@^5.8.3": "5.8.3", - "npm:vite-plugin-i18n-ally@^6.0.1": "6.0.1_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@types+node@22.15.3", - "npm:vite-plugin-node-polyfills@0.23": "0.23.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@types+node@22.15.3", - "npm:vite-plugin-pwa@1": "1.0.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_workbox-build@7.3.0__ajv@8.17.1__@babel+core@7.27.1__rollup@2.79.2_workbox-window@7.3.0_@types+node@22.15.3", - "npm:vite-plugin-static-copy@3": "3.0.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@types+node@22.15.3", - "npm:vite@^6.3.4": "6.3.4_@types+node@22.15.3_picomatch@4.0.2", - "npm:vitest@^3.1.2": "3.1.2_@types+node@22.15.3_happy-dom@17.4.6_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2", - "npm:zod@^3.24.3": "3.24.3", - "npm:zustand@5.0.4": "5.0.4_@types+react@19.1.2_immer@10.1.1_react@19.1.0" + "npm:vite@7": "7.0.2_@types+node@22.16.0_picomatch@4.0.2", + "npm:vitest@^3.2.4": "3.2.4_@types+node@22.16.0_happy-dom@18.0.1_vite@7.0.2__@types+node@22.16.0__picomatch@4.0.2", + "npm:zod@^3.25.75": "3.25.75", + "npm:zustand@5.0.6": "5.0.6_@types+react@19.1.8_immer@10.1.1_react@19.1.0" }, "jsr": { - "@std/path@1.0.6": { - "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" + "@meshtastic/protobufs@2.7.0": { + "integrity": "38357241bd8a7431c87366dbe12ce9e69f204ebb6ec23da12f7682765b6c8376", + "dependencies": [ + "npm:@bufbuild/protobuf@^2.2.3" + ] }, - "@std/path@1.1.0": { - "integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" + "@std/internal@1.0.9": { + "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8" + }, + "@std/path@1.1.1": { + "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", + "dependencies": [ + "jsr:@std/internal" + ] } }, "npm": { - "@adobe/css-tools@4.4.2": { - "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==" + "@adobe/css-tools@4.4.3": { + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==" }, "@alloc/quick-lru@5.2.0": { "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" @@ -119,28 +140,19 @@ "zone.js" ] }, - "@apideck/better-ajv-errors@0.3.6_ajv@8.17.1": { - "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", - "dependencies": [ - "ajv", - "json-schema", - "jsonpointer", - "leven" - ] - }, "@babel/code-frame@7.27.1": { "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dependencies": [ "@babel/helper-validator-identifier", - "js-tokens", + "js-tokens@4.0.0", "picocolors" ] }, - "@babel/compat-data@7.27.1": { - "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==" + "@babel/compat-data@7.28.0": { + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==" }, - "@babel/core@7.27.1": { - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "@babel/core@7.28.0": { + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dependencies": [ "@ampproject/remapping", "@babel/code-frame", @@ -159,24 +171,24 @@ "semver" ] }, - "@babel/generator@7.27.1": { - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "@babel/generator@7.28.0": { + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dependencies": [ "@babel/parser", "@babel/types", "@jridgewell/gen-mapping", "@jridgewell/trace-mapping", - "jsesc@3.1.0" + "jsesc" ] }, - "@babel/helper-annotate-as-pure@7.27.1": { - "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "@babel/helper-annotate-as-pure@7.27.3": { + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dependencies": [ "@babel/types" ] }, - "@babel/helper-compilation-targets@7.27.1": { - "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", + "@babel/helper-compilation-targets@7.27.2": { + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dependencies": [ "@babel/compat-data", "@babel/helper-validator-option", @@ -185,7 +197,7 @@ "semver" ] }, - "@babel/helper-create-class-features-plugin@7.27.1_@babel+core@7.27.1": { + "@babel/helper-create-class-features-plugin@7.27.1_@babel+core@7.28.0": { "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dependencies": [ "@babel/core", @@ -198,25 +210,8 @@ "semver" ] }, - "@babel/helper-create-regexp-features-plugin@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-annotate-as-pure", - "regexpu-core", - "semver" - ] - }, - "@babel/helper-define-polyfill-provider@0.6.4_@babel+core@7.27.1": { - "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", - "dependencies": [ - "@babel/core", - "@babel/helper-compilation-targets", - "@babel/helper-plugin-utils", - "debug", - "lodash.debounce", - "resolve" - ] + "@babel/helper-globals@7.28.0": { + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" }, "@babel/helper-member-expression-to-functions@7.27.1": { "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", @@ -232,8 +227,8 @@ "@babel/types" ] }, - "@babel/helper-module-transforms@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "@babel/helper-module-transforms@7.27.3_@babel+core@7.28.0": { + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dependencies": [ "@babel/core", "@babel/helper-module-imports", @@ -250,16 +245,7 @@ "@babel/helper-plugin-utils@7.27.1": { "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==" }, - "@babel/helper-remap-async-to-generator@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dependencies": [ - "@babel/core", - "@babel/helper-annotate-as-pure", - "@babel/helper-wrap-function", - "@babel/traverse" - ] - }, - "@babel/helper-replace-supers@7.27.1_@babel+core@7.27.1": { + "@babel/helper-replace-supers@7.27.1_@babel+core@7.28.0": { "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dependencies": [ "@babel/core", @@ -284,289 +270,35 @@ "@babel/helper-validator-option@7.27.1": { "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" }, - "@babel/helper-wrap-function@7.27.1": { - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "@babel/helpers@7.27.6": { + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dependencies": [ "@babel/template", - "@babel/traverse", "@babel/types" ] }, - "@babel/helpers@7.27.1": { - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "dependencies": [ - "@babel/template", - "@babel/types" - ] - }, - "@babel/parser@7.27.1": { - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "@babel/parser@7.28.0": { + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dependencies": [ "@babel/types" ], "bin": true }, - "@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/traverse" - ] - }, - "@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/helper-skip-transparent-expression-wrappers", - "@babel/plugin-transform-optional-chaining" - ] - }, - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/traverse" - ] - }, - "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2_@babel+core@7.27.1": { - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dependencies": [ - "@babel/core" - ] - }, - "@babel/plugin-syntax-import-assertions@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-syntax-import-attributes@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-syntax-jsx@7.27.1_@babel+core@7.27.1": { + "@babel/plugin-syntax-jsx@7.27.1_@babel+core@7.28.0": { "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dependencies": [ "@babel/core", "@babel/helper-plugin-utils" ] }, - "@babel/plugin-syntax-typescript@7.27.1_@babel+core@7.27.1": { + "@babel/plugin-syntax-typescript@7.27.1_@babel+core@7.28.0": { "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dependencies": [ "@babel/core", "@babel/helper-plugin-utils" ] }, - "@babel/plugin-syntax-unicode-sets-regex@7.18.6_@babel+core@7.27.1": { - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-arrow-functions@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-async-generator-functions@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/helper-remap-async-to-generator", - "@babel/traverse" - ] - }, - "@babel/plugin-transform-async-to-generator@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "dependencies": [ - "@babel/core", - "@babel/helper-module-imports", - "@babel/helper-plugin-utils", - "@babel/helper-remap-async-to-generator" - ] - }, - "@babel/plugin-transform-block-scoped-functions@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-block-scoping@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-class-properties@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-class-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-class-static-block@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-class-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-classes@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", - "dependencies": [ - "@babel/core", - "@babel/helper-annotate-as-pure", - "@babel/helper-compilation-targets", - "@babel/helper-plugin-utils", - "@babel/helper-replace-supers", - "@babel/traverse", - "globals" - ] - }, - "@babel/plugin-transform-computed-properties@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/template" - ] - }, - "@babel/plugin-transform-destructuring@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-dotall-regex@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-duplicate-keys@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-dynamic-import@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-exponentiation-operator@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-export-namespace-from@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-for-of@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/helper-skip-transparent-expression-wrappers" - ] - }, - "@babel/plugin-transform-function-name@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-compilation-targets", - "@babel/helper-plugin-utils", - "@babel/traverse" - ] - }, - "@babel/plugin-transform-json-strings@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-literals@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-logical-assignment-operators@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-member-expression-literals@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-modules-amd@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dependencies": [ - "@babel/core", - "@babel/helper-module-transforms", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-modules-commonjs@7.27.1_@babel+core@7.27.1": { + "@babel/plugin-transform-modules-commonjs@7.27.1_@babel+core@7.28.0": { "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dependencies": [ "@babel/core", @@ -574,503 +306,247 @@ "@babel/helper-plugin-utils" ] }, - "@babel/plugin-transform-modules-systemjs@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", - "dependencies": [ - "@babel/core", - "@babel/helper-module-transforms", - "@babel/helper-plugin-utils", - "@babel/helper-validator-identifier", - "@babel/traverse" - ] - }, - "@babel/plugin-transform-modules-umd@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dependencies": [ - "@babel/core", - "@babel/helper-module-transforms", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-named-capturing-groups-regex@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-new-target@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-nullish-coalescing-operator@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-numeric-separator@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-object-rest-spread@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", - "dependencies": [ - "@babel/core", - "@babel/helper-compilation-targets", - "@babel/helper-plugin-utils", - "@babel/plugin-transform-parameters" - ] - }, - "@babel/plugin-transform-object-super@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/helper-replace-supers" - ] - }, - "@babel/plugin-transform-optional-catch-binding@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-optional-chaining@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/helper-skip-transparent-expression-wrappers" - ] - }, - "@babel/plugin-transform-parameters@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-private-methods@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-class-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-private-property-in-object@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-annotate-as-pure", - "@babel/helper-create-class-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-property-literals@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-react-jsx-self@7.27.1_@babel+core@7.27.1": { + "@babel/plugin-transform-react-jsx-self@7.27.1_@babel+core@7.28.0": { "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dependencies": [ "@babel/core", "@babel/helper-plugin-utils" ] }, - "@babel/plugin-transform-react-jsx-source@7.27.1_@babel+core@7.27.1": { + "@babel/plugin-transform-react-jsx-source@7.27.1_@babel+core@7.28.0": { "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dependencies": [ "@babel/core", "@babel/helper-plugin-utils" ] }, - "@babel/plugin-transform-regenerator@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-regexp-modifiers@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-reserved-words@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-shorthand-properties@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-spread@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "@babel/plugin-transform-typescript@7.28.0_@babel+core@7.28.0": { + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", "dependencies": [ "@babel/core", + "@babel/helper-annotate-as-pure", + "@babel/helper-create-class-features-plugin", "@babel/helper-plugin-utils", - "@babel/helper-skip-transparent-expression-wrappers" - ] - }, - "@babel/plugin-transform-sticky-regex@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-template-literals@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-typeof-symbol@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-unicode-escapes@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-unicode-property-regex@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-unicode-regex@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" - ] - }, - "@babel/plugin-transform-unicode-sets-regex@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "dependencies": [ - "@babel/core", - "@babel/helper-create-regexp-features-plugin", - "@babel/helper-plugin-utils" + "@babel/helper-skip-transparent-expression-wrappers", + "@babel/plugin-syntax-typescript" ] }, - "@babel/preset-env@7.27.1_@babel+core@7.27.1": { - "integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", + "@babel/preset-typescript@7.27.1_@babel+core@7.28.0": { + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", "dependencies": [ - "@babel/compat-data", "@babel/core", - "@babel/helper-compilation-targets", "@babel/helper-plugin-utils", "@babel/helper-validator-option", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key", - "@babel/plugin-bugfix-safari-class-field-initializer-scope", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly", - "@babel/plugin-proposal-private-property-in-object", - "@babel/plugin-syntax-import-assertions", - "@babel/plugin-syntax-import-attributes", - "@babel/plugin-syntax-unicode-sets-regex", - "@babel/plugin-transform-arrow-functions", - "@babel/plugin-transform-async-generator-functions", - "@babel/plugin-transform-async-to-generator", - "@babel/plugin-transform-block-scoped-functions", - "@babel/plugin-transform-block-scoping", - "@babel/plugin-transform-class-properties", - "@babel/plugin-transform-class-static-block", - "@babel/plugin-transform-classes", - "@babel/plugin-transform-computed-properties", - "@babel/plugin-transform-destructuring", - "@babel/plugin-transform-dotall-regex", - "@babel/plugin-transform-duplicate-keys", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex", - "@babel/plugin-transform-dynamic-import", - "@babel/plugin-transform-exponentiation-operator", - "@babel/plugin-transform-export-namespace-from", - "@babel/plugin-transform-for-of", - "@babel/plugin-transform-function-name", - "@babel/plugin-transform-json-strings", - "@babel/plugin-transform-literals", - "@babel/plugin-transform-logical-assignment-operators", - "@babel/plugin-transform-member-expression-literals", - "@babel/plugin-transform-modules-amd", + "@babel/plugin-syntax-jsx", "@babel/plugin-transform-modules-commonjs", - "@babel/plugin-transform-modules-systemjs", - "@babel/plugin-transform-modules-umd", - "@babel/plugin-transform-named-capturing-groups-regex", - "@babel/plugin-transform-new-target", - "@babel/plugin-transform-nullish-coalescing-operator", - "@babel/plugin-transform-numeric-separator", - "@babel/plugin-transform-object-rest-spread", - "@babel/plugin-transform-object-super", - "@babel/plugin-transform-optional-catch-binding", - "@babel/plugin-transform-optional-chaining", - "@babel/plugin-transform-parameters", - "@babel/plugin-transform-private-methods", - "@babel/plugin-transform-private-property-in-object", - "@babel/plugin-transform-property-literals", - "@babel/plugin-transform-regenerator", - "@babel/plugin-transform-regexp-modifiers", - "@babel/plugin-transform-reserved-words", - "@babel/plugin-transform-shorthand-properties", - "@babel/plugin-transform-spread", - "@babel/plugin-transform-sticky-regex", - "@babel/plugin-transform-template-literals", - "@babel/plugin-transform-typeof-symbol", - "@babel/plugin-transform-unicode-escapes", - "@babel/plugin-transform-unicode-property-regex", - "@babel/plugin-transform-unicode-regex", - "@babel/plugin-transform-unicode-sets-regex", - "@babel/preset-modules", - "babel-plugin-polyfill-corejs2", - "babel-plugin-polyfill-corejs3", - "babel-plugin-polyfill-regenerator", - "core-js-compat", - "semver" - ] - }, - "@babel/preset-modules@0.1.6-no-external-plugins_@babel+core@7.27.1": { - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dependencies": [ - "@babel/core", - "@babel/helper-plugin-utils", - "@babel/types", - "esutils" + "@babel/plugin-transform-typescript" ] }, - "@babel/runtime@7.27.1": { - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==" + "@babel/runtime@7.27.6": { + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==" }, - "@babel/template@7.27.1": { - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "@babel/template@7.27.2": { + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dependencies": [ "@babel/code-frame", "@babel/parser", "@babel/types" ] }, - "@babel/traverse@7.27.1": { - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "@babel/traverse@7.28.0": { + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dependencies": [ "@babel/code-frame", "@babel/generator", + "@babel/helper-globals", "@babel/parser", "@babel/template", "@babel/types", - "debug", - "globals" + "debug" ] }, - "@babel/types@7.27.1": { - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "@babel/types@7.28.0": { + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "dependencies": [ "@babel/helper-string-parser", "@babel/helper-validator-identifier" ] }, - "@bufbuild/protobuf@2.2.5": { - "integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==" + "@bufbuild/protobuf@2.6.0": { + "integrity": "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==" }, - "@emnapi/core@1.4.3": { - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "@emnapi/core@1.4.4": { + "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", "dependencies": [ "@emnapi/wasi-threads", "tslib@2.8.1" ] }, - "@emnapi/runtime@1.4.3": { - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "@emnapi/runtime@1.4.4": { + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", "dependencies": [ "tslib@2.8.1" ] }, - "@emnapi/wasi-threads@1.0.2": { - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "@emnapi/wasi-threads@1.0.3": { + "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", "dependencies": [ "tslib@2.8.1" ] }, - "@esbuild/aix-ppc64@0.25.3": { - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "@esbuild/aix-ppc64@0.25.6": { + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "os": ["aix"], "cpu": ["ppc64"] }, - "@esbuild/android-arm64@0.25.3": { - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "@esbuild/android-arm64@0.25.6": { + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "os": ["android"], "cpu": ["arm64"] }, - "@esbuild/android-arm@0.25.3": { - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "@esbuild/android-arm@0.25.6": { + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "os": ["android"], "cpu": ["arm"] }, - "@esbuild/android-x64@0.25.3": { - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "@esbuild/android-x64@0.25.6": { + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "os": ["android"], "cpu": ["x64"] }, - "@esbuild/darwin-arm64@0.25.3": { - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "@esbuild/darwin-arm64@0.25.6": { + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "os": ["darwin"], "cpu": ["arm64"] }, - "@esbuild/darwin-x64@0.25.3": { - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "@esbuild/darwin-x64@0.25.6": { + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "os": ["darwin"], "cpu": ["x64"] }, - "@esbuild/freebsd-arm64@0.25.3": { - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "@esbuild/freebsd-arm64@0.25.6": { + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@esbuild/freebsd-x64@0.25.3": { - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "@esbuild/freebsd-x64@0.25.6": { + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "os": ["freebsd"], "cpu": ["x64"] }, - "@esbuild/linux-arm64@0.25.3": { - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "@esbuild/linux-arm64@0.25.6": { + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@esbuild/linux-arm@0.25.3": { - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "@esbuild/linux-arm@0.25.6": { + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "os": ["linux"], "cpu": ["arm"] }, - "@esbuild/linux-ia32@0.25.3": { - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "@esbuild/linux-ia32@0.25.6": { + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "os": ["linux"], "cpu": ["ia32"] }, - "@esbuild/linux-loong64@0.25.3": { - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "@esbuild/linux-loong64@0.25.6": { + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "os": ["linux"], "cpu": ["loong64"] }, - "@esbuild/linux-mips64el@0.25.3": { - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "@esbuild/linux-mips64el@0.25.6": { + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "os": ["linux"], "cpu": ["mips64el"] }, - "@esbuild/linux-ppc64@0.25.3": { - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "@esbuild/linux-ppc64@0.25.6": { + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "os": ["linux"], "cpu": ["ppc64"] }, - "@esbuild/linux-riscv64@0.25.3": { - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "@esbuild/linux-riscv64@0.25.6": { + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "os": ["linux"], "cpu": ["riscv64"] }, - "@esbuild/linux-s390x@0.25.3": { - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "@esbuild/linux-s390x@0.25.6": { + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "os": ["linux"], "cpu": ["s390x"] }, - "@esbuild/linux-x64@0.25.3": { - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "@esbuild/linux-x64@0.25.6": { + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "os": ["linux"], "cpu": ["x64"] }, - "@esbuild/netbsd-arm64@0.25.3": { - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "@esbuild/netbsd-arm64@0.25.6": { + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "os": ["netbsd"], "cpu": ["arm64"] }, - "@esbuild/netbsd-x64@0.25.3": { - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "@esbuild/netbsd-x64@0.25.6": { + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "os": ["netbsd"], "cpu": ["x64"] }, - "@esbuild/openbsd-arm64@0.25.3": { - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "@esbuild/openbsd-arm64@0.25.6": { + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "os": ["openbsd"], "cpu": ["arm64"] }, - "@esbuild/openbsd-x64@0.25.3": { - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "@esbuild/openbsd-x64@0.25.6": { + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "os": ["openbsd"], "cpu": ["x64"] }, - "@esbuild/sunos-x64@0.25.3": { - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "@esbuild/openharmony-arm64@0.25.6": { + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/sunos-x64@0.25.6": { + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "os": ["sunos"], "cpu": ["x64"] }, - "@esbuild/win32-arm64@0.25.3": { - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "@esbuild/win32-arm64@0.25.6": { + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "os": ["win32"], "cpu": ["arm64"] }, - "@esbuild/win32-ia32@0.25.3": { - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "@esbuild/win32-ia32@0.25.6": { + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "os": ["win32"], "cpu": ["ia32"] }, - "@esbuild/win32-x64@0.25.3": { - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "@esbuild/win32-x64@0.25.6": { + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "os": ["win32"], "cpu": ["x64"] }, - "@floating-ui/core@1.7.0": { - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "@floating-ui/core@1.7.2": { + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", "dependencies": [ "@floating-ui/utils" ] }, - "@floating-ui/dom@1.7.0": { - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "@floating-ui/dom@1.7.2": { + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", "dependencies": [ "@floating-ui/core", "@floating-ui/utils" ] }, - "@floating-ui/react-dom@2.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "@floating-ui/react-dom@2.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", "dependencies": [ "@floating-ui/dom", "react", "react-dom" ] }, - "@floating-ui/utils@0.2.9": { - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + "@floating-ui/utils@0.2.10": { + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" }, "@gfx/zopfli@1.0.15": { "integrity": "sha512-7mBgpi7UD82fsff5ThQKet0uBTl4BYerQuc+/qA1ELTwWEiIedRTcD3JgiUu9wwZ2kytW8JOb165rSdAt8PfcQ==", @@ -1078,16 +554,22 @@ "base64-js" ] }, + "@hookform/resolvers@5.1.1_react-hook-form@7.60.0__react@19.1.0_react@19.1.0": { + "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "dependencies": [ + "@standard-schema/utils", + "react-hook-form" + ] + }, "@isaacs/fs-minipass@4.0.1": { "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dependencies": [ "minipass" ] }, - "@jridgewell/gen-mapping@0.3.8": { - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "@jridgewell/gen-mapping@0.3.12": { + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dependencies": [ - "@jridgewell/set-array", "@jridgewell/sourcemap-codec", "@jridgewell/trace-mapping" ] @@ -1095,28 +577,18 @@ "@jridgewell/resolve-uri@3.1.2": { "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" }, - "@jridgewell/set-array@1.2.1": { - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + "@jridgewell/sourcemap-codec@1.5.4": { + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" }, - "@jridgewell/source-map@0.3.6": { - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dependencies": [ - "@jridgewell/gen-mapping", - "@jridgewell/trace-mapping" - ] - }, - "@jridgewell/sourcemap-codec@1.5.0": { - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "@jridgewell/trace-mapping@0.3.25": { - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "@jridgewell/trace-mapping@0.3.29": { + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dependencies": [ "@jridgewell/resolve-uri", "@jridgewell/sourcemap-codec" ] }, - "@jsr/meshtastic__core@2.6.2": { - "integrity": "sha512-mzsxs9hQeVimQd/tj15Ojw5FYY8Iko3EbDviFZoEw3bmXgiySG53GvQsNA9wV13lFbDu96SM3uK/LI3EHMFY9w==", + "@jsr/meshtastic__core@2.6.4": { + "integrity": "sha512-1Kz5DK6peFxluHOJR38vFwfgeJzMXTz+3p6TvibjILVhSQC2U1nu8aJbn6w5zhRqS+j79OmtrRvdzL6VNsTkkQ==", "dependencies": [ "@bufbuild/protobuf", "@jsr/meshtastic__protobufs", @@ -1124,14 +596,46 @@ "ste-simple-events", "tslog" ], - "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__core/2.6.2.tgz" + "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__core/2.6.4.tgz" }, - "@jsr/meshtastic__protobufs@2.6.2": { - "integrity": "sha512-bIENtFnUEru28GrAeSdiBS9skp0hN/3HZunMbF/IjvUrXOlx2fptKVj3b+pzjOWnLBZxllrByV/W+XDmrxqJ6g==", + "@jsr/meshtastic__js@2.6.0-0": { + "integrity": "sha512-+xpZpxK6oUIVOuEs7C+LyxRr2druvc7UNNNTK9Rl8ioXj63Jz1uQXlYe2Gj0xjnRAiSQLR7QVaPef21BR/YTxA==", + "dependencies": [ + "@bufbuild/protobuf", + "@jsr/meshtastic__protobufs", + "crc", + "ste-simple-events", + "tslog" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__js/2.6.0-0.tgz" + }, + "@jsr/meshtastic__protobufs@2.7.0": { + "integrity": "sha512-ndZhUyB/ADSyjJI+iSeSOoIKqNGZ2+ERVjfY0qnh4jgF740tFTwefC5mzZhOqDLbreGFYS79+429NtH5Ujdzdg==", "dependencies": [ "@bufbuild/protobuf" ], - "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__protobufs/2.6.2.tgz" + "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__protobufs/2.7.0.tgz" + }, + "@jsr/meshtastic__transport-http@0.2.1": { + "integrity": "sha512-lmQKr3aIINKvtGROU4HchmSVqbZSbkIHqajowRRC8IAjsnR0zNTyxz210QyY4pFUF9hpcW3GRjwq5h/VO2JuGg==", + "dependencies": [ + "@jsr/meshtastic__core" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-http/0.2.1.tgz" + }, + "@jsr/meshtastic__transport-web-bluetooth@0.1.2": { + "integrity": "sha512-Z+5pv9RXNgY0/crKExOH3pZ6LT0HIXFmnBL7NX5AO2knOFRn+4lmxQEhhmiTTlkUfqyEfAvbjuY5u4mq9TPTdQ==", + "dependencies": [ + "@jsr/meshtastic__core" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-web-bluetooth/0.1.2.tgz" + }, + "@jsr/meshtastic__transport-web-serial@0.2.1": { + "integrity": "sha512-yumjEGLkAuJYOC3aWKvZzbQqi/LnqaKfNpVCY7Ki7oLtAshNiZrBLiwiFhN7+ZR9FfMdJThyBMqREBDRRWTO1Q==", + "dependencies": [ + "@jsr/meshtastic__core" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-web-serial/0.2.1.tgz" }, "@mapbox/geojson-rewind@0.5.2": { "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", @@ -1174,8 +678,8 @@ ], "bin": true }, - "@maplibre/maplibre-gl-style-spec@23.2.2": { - "integrity": "sha512-kLcVlItPCULc20SM6pSVA7u8nST9xmQA8d7utc9j3KB0Tf/xhM4GgCn/QsZcmlbN/wW0ujyomDrvZ3/LbwvAmw==", + "@maplibre/maplibre-gl-style-spec@23.3.0": { + "integrity": "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA==", "dependencies": [ "@mapbox/jsonlint-lines-primitives", "@mapbox/unitbezier", @@ -1187,16 +691,16 @@ ], "bin": true }, - "@napi-rs/wasm-runtime@0.2.9": { - "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "@napi-rs/wasm-runtime@0.2.11": { + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", "dependencies": [ "@emnapi/core", "@emnapi/runtime", "@tybys/wasm-util" ] }, - "@noble/curves@1.9.0": { - "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "@noble/curves@1.9.2": { + "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", "dependencies": [ "@noble/hashes" ] @@ -1204,40 +708,23 @@ "@noble/hashes@1.8.0": { "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" }, - "@nodelib/fs.scandir@2.1.5": { - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": [ - "@nodelib/fs.stat", - "run-parallel" - ] - }, - "@nodelib/fs.stat@2.0.5": { - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - }, - "@nodelib/fs.walk@1.2.8": { - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": [ - "@nodelib/fs.scandir", - "fastq" - ] - }, "@radix-ui/number@1.1.1": { "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" }, "@radix-ui/primitive@1.1.2": { "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==" }, - "@radix-ui/react-accordion@1.2.8_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-c7OKBvO36PfQIUGIjj1Wko0hH937pYFU2tR5zbIJDUsmTzHoZVHHt4bmb7OOJbzTaWJtVELKWojBHa7OcnUHmQ==", + "@radix-ui/react-accordion@1.2.11_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-collapsible", - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-collection", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-direction", "@radix-ui/react-id", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-controllable-state", "@types/react", "@types/react-dom", @@ -1249,10 +736,10 @@ "@types/react-dom" ] }, - "@radix-ui/react-arrow@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", + "@radix-ui/react-arrow@1.1.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "dependencies": [ - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@types/react", "@types/react-dom", "react", @@ -1263,14 +750,14 @@ "@types/react-dom" ] }, - "@radix-ui/react-checkbox@1.2.3_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-pHVzDYsnaDmBlAuwim45y3soIN8H4R7KbkSVirGhXO+R/kO2OLCe0eucUEbddaTcdMHHdzcIGHtZSMSQlA+apw==", + "@radix-ui/react-checkbox@1.3.2_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-controllable-state", "@radix-ui/react-use-previous", "@radix-ui/react-use-size", @@ -1284,15 +771,15 @@ "@types/react-dom" ] }, - "@radix-ui/react-collapsible@1.1.8_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-hxEsLvK9WxIAPyxdDRULL4hcaSjMZCfP7fHB0Z1uUnDoDBat1Zh46hwYfa69DeZAbJrPckjf0AGAtEZyvDyJbw==", + "@radix-ui/react-collapsible@1.1.11_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-id", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-controllable-state", "@radix-ui/react-use-layout-effect", "@types/react", @@ -1305,30 +792,13 @@ "@types/react-dom" ] }, - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0", - "@types/react", - "@types/react-dom", - "react", - "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] - }, - "@radix-ui/react-collection@1.1.7_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@radix-ui/react-collection@1.1.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "dependencies": [ "@radix-ui/react-compose-refs", "@radix-ui/react-context", - "@radix-ui/react-primitive@2.1.3_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-slot@1.2.3_@types+react@19.1.2_react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", "@types/react", "@types/react-dom", "react", @@ -1339,7 +809,7 @@ "@types/react-dom" ] }, - "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "dependencies": [ "@types/react", @@ -1349,7 +819,7 @@ "@types/react" ] }, - "@radix-ui/react-context@1.1.2_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-context@1.1.2_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "dependencies": [ "@types/react", @@ -1359,8 +829,8 @@ "@types/react" ] }, - "@radix-ui/react-dialog@1.1.11_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", + "@radix-ui/react-dialog@1.1.14_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", @@ -1371,8 +841,8 @@ "@radix-ui/react-id", "@radix-ui/react-portal", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", "@radix-ui/react-use-controllable-state", "@types/react", "@types/react-dom", @@ -1386,7 +856,7 @@ "@types/react-dom" ] }, - "@radix-ui/react-direction@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-direction@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "dependencies": [ "@types/react", @@ -1396,12 +866,12 @@ "@types/react" ] }, - "@radix-ui/react-dismissable-layer@1.1.7_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", + "@radix-ui/react-dismissable-layer@1.1.10_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-callback-ref", "@radix-ui/react-use-escape-keydown", "@types/react", @@ -1414,15 +884,15 @@ "@types/react-dom" ] }, - "@radix-ui/react-dropdown-menu@2.1.12_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA==", + "@radix-ui/react-dropdown-menu@2.1.15_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-id", "@radix-ui/react-menu", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-controllable-state", "@types/react", "@types/react-dom", @@ -1434,7 +904,7 @@ "@types/react-dom" ] }, - "@radix-ui/react-focus-guards@1.1.2_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-focus-guards@1.1.2_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", "dependencies": [ "@types/react", @@ -1444,11 +914,11 @@ "@types/react" ] }, - "@radix-ui/react-focus-scope@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", + "@radix-ui/react-focus-scope@1.1.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "dependencies": [ "@radix-ui/react-compose-refs", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-callback-ref", "@types/react", "@types/react-dom", @@ -1460,7 +930,7 @@ "@types/react-dom" ] }, - "@radix-ui/react-id@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-id@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "dependencies": [ "@radix-ui/react-use-layout-effect", @@ -1471,10 +941,10 @@ "@types/react" ] }, - "@radix-ui/react-label@2.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==", + "@radix-ui/react-label@2.1.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", "dependencies": [ - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@types/react", "@types/react-dom", "react", @@ -1485,11 +955,11 @@ "@types/react-dom" ] }, - "@radix-ui/react-menu@2.1.12_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q==", + "@radix-ui/react-menu@2.1.15_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", "dependencies": [ "@radix-ui/primitive", - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-collection", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-direction", @@ -1500,9 +970,9 @@ "@radix-ui/react-popper", "@radix-ui/react-portal", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-roving-focus@1.1.7_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-slot", "@radix-ui/react-use-callback-ref", "@types/react", "@types/react-dom", @@ -1516,18 +986,18 @@ "@types/react-dom" ] }, - "@radix-ui/react-menubar@1.1.12_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-bM2vT5nxRqJH/d1vFQ9jLsW4qR70yFQw2ZD1TUPWUNskDsV0eYeMbbNJqxNjGMOVogEkOJaHtu11kzYdTJvVJg==", + "@radix-ui/react-menubar@1.1.15_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A==", "dependencies": [ "@radix-ui/primitive", - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-collection", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-direction", "@radix-ui/react-id", "@radix-ui/react-menu", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-roving-focus@1.1.7_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", "@radix-ui/react-use-controllable-state", "@types/react", "@types/react-dom", @@ -1539,8 +1009,8 @@ "@types/react-dom" ] }, - "@radix-ui/react-popover@1.1.11_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-yFMfZkVA5G3GJnBgb2PxrrcLKm1ZLWXrbYVgdyTl//0TYEIHS9LJbnyz7WWcZ0qCq7hIlJZpRtxeSeIG5T5oJw==", + "@radix-ui/react-popover@1.1.14_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", @@ -1552,8 +1022,8 @@ "@radix-ui/react-popper", "@radix-ui/react-portal", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", "@radix-ui/react-use-controllable-state", "@types/react", "@types/react-dom", @@ -1567,14 +1037,14 @@ "@types/react-dom" ] }, - "@radix-ui/react-popper@1.2.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==", + "@radix-ui/react-popper@1.2.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", "dependencies": [ "@floating-ui/react-dom", "@radix-ui/react-arrow", "@radix-ui/react-compose-refs", "@radix-ui/react-context", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-callback-ref", "@radix-ui/react-use-layout-effect", "@radix-ui/react-use-rect", @@ -1590,10 +1060,10 @@ "@types/react-dom" ] }, - "@radix-ui/react-portal@1.1.6_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", + "@radix-ui/react-portal@1.1.9_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "dependencies": [ - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-layout-effect", "@types/react", "@types/react-dom", @@ -1605,7 +1075,7 @@ "@types/react-dom" ] }, - "@radix-ui/react-presence@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@radix-ui/react-presence@1.1.4_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", "dependencies": [ "@radix-ui/react-compose-refs", @@ -1620,24 +1090,10 @@ "@types/react-dom" ] }, - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", - "dependencies": [ - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0", - "@types/react", - "@types/react-dom", - "react", - "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] - }, - "@radix-ui/react-primitive@2.1.3_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@radix-ui/react-primitive@2.1.3_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "dependencies": [ - "@radix-ui/react-slot@1.2.3_@types+react@19.1.2_react@19.1.0", + "@radix-ui/react-slot", "@types/react", "@types/react-dom", "react", @@ -1648,16 +1104,16 @@ "@types/react-dom" ] }, - "@radix-ui/react-roving-focus@1.1.10_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@radix-ui/react-roving-focus@1.1.10_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", "dependencies": [ "@radix-ui/primitive", - "@radix-ui/react-collection@1.1.7_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-collection", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-direction", "@radix-ui/react-id", - "@radix-ui/react-primitive@2.1.3_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-callback-ref", "@radix-ui/react-use-controllable-state", "@types/react", @@ -1670,30 +1126,8 @@ "@types/react-dom" ] }, - "@radix-ui/react-roving-focus@1.1.7_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==", - "dependencies": [ - "@radix-ui/primitive", - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-compose-refs", - "@radix-ui/react-context", - "@radix-ui/react-direction", - "@radix-ui/react-id", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-use-callback-ref", - "@radix-ui/react-use-controllable-state", - "@types/react", - "@types/react-dom", - "react", - "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" - ] - }, - "@radix-ui/react-scroll-area@1.2.6_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-lj8OMlpPERXrQIHlEQdlXHJoRT52AMpBrgyPYylOhXYq5e/glsEdtOc/kCQlsTdtgN5U0iDbrrolDadvektJGQ==", + "@radix-ui/react-scroll-area@1.2.9_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", "dependencies": [ "@radix-ui/number", "@radix-ui/primitive", @@ -1701,7 +1135,7 @@ "@radix-ui/react-context", "@radix-ui/react-direction", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-callback-ref", "@radix-ui/react-use-layout-effect", "@types/react", @@ -1714,12 +1148,12 @@ "@types/react-dom" ] }, - "@radix-ui/react-select@2.2.2_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA==", + "@radix-ui/react-select@2.2.5_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", "dependencies": [ "@radix-ui/number", "@radix-ui/primitive", - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-collection", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-direction", @@ -1729,8 +1163,8 @@ "@radix-ui/react-id", "@radix-ui/react-popper", "@radix-ui/react-portal", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", "@radix-ui/react-use-callback-ref", "@radix-ui/react-use-controllable-state", "@radix-ui/react-use-layout-effect", @@ -1748,10 +1182,10 @@ "@types/react-dom" ] }, - "@radix-ui/react-separator@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w==", + "@radix-ui/react-separator@1.1.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", "dependencies": [ - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@types/react", "@types/react-dom", "react", @@ -1762,16 +1196,16 @@ "@types/react-dom" ] }, - "@radix-ui/react-slider@1.3.2_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-oQnqfgSiYkxZ1MrF6672jw2/zZvpB+PJsrIc3Zm1zof1JHf/kj7WhmROw7JahLfOwYQ5/+Ip0rFORgF1tjSiaQ==", + "@radix-ui/react-slider@1.3.5_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==", "dependencies": [ "@radix-ui/number", "@radix-ui/primitive", - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-collection", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-direction", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-controllable-state", "@radix-ui/react-use-layout-effect", "@radix-ui/react-use-previous", @@ -1786,18 +1220,7 @@ "@types/react-dom" ] }, - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0": { - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", - "dependencies": [ - "@radix-ui/react-compose-refs", - "@types/react", - "react" - ], - "optionalPeers": [ - "@types/react" - ] - }, - "@radix-ui/react-slot@1.2.3_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-slot@1.2.3_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "dependencies": [ "@radix-ui/react-compose-refs", @@ -1808,13 +1231,13 @@ "@types/react" ] }, - "@radix-ui/react-switch@1.2.2_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==", + "@radix-ui/react-switch@1.2.5_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", "@radix-ui/react-context", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-controllable-state", "@radix-ui/react-use-previous", "@radix-ui/react-use-size", @@ -1828,16 +1251,16 @@ "@types/react-dom" ] }, - "@radix-ui/react-tabs@1.1.9_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-KIjtwciYvquiW/wAFkELZCVnaNLBsYNhTNcvl+zfMAbMhRkcvNuCLXDDd22L0j7tagpzVh/QwbFpwAATg7ILPw==", + "@radix-ui/react-tabs@1.1.12_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-context", "@radix-ui/react-direction", "@radix-ui/react-id", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-roving-focus@1.1.7_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", "@radix-ui/react-use-controllable-state", "@types/react", "@types/react-dom", @@ -1849,17 +1272,17 @@ "@types/react-dom" ] }, - "@radix-ui/react-toast@1.2.11_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-Ed2mlOmT+tktOsu2NZBK1bCSHh/uqULu1vWOkpQTVq53EoOuZUZw7FInQoDB3uil5wZc2oe0XN9a7uVZB7/6AQ==", + "@radix-ui/react-toast@1.2.14_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", "dependencies": [ "@radix-ui/primitive", - "@radix-ui/react-collection@1.1.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-collection", "@radix-ui/react-compose-refs", "@radix-ui/react-context", "@radix-ui/react-dismissable-layer", "@radix-ui/react-portal", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-callback-ref", "@radix-ui/react-use-controllable-state", "@radix-ui/react-use-layout-effect", @@ -1874,14 +1297,14 @@ "@types/react-dom" ] }, - "@radix-ui/react-toggle-group@1.1.10_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@radix-ui/react-toggle-group@1.1.10_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-context", "@radix-ui/react-direction", - "@radix-ui/react-primitive@2.1.3_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-roving-focus@1.1.10_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", "@radix-ui/react-toggle", "@radix-ui/react-use-controllable-state", "@types/react", @@ -1894,11 +1317,11 @@ "@types/react-dom" ] }, - "@radix-ui/react-toggle@1.1.9_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@radix-ui/react-toggle@1.1.9_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==", "dependencies": [ "@radix-ui/primitive", - "@radix-ui/react-primitive@2.1.3_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@radix-ui/react-use-controllable-state", "@types/react", "@types/react-dom", @@ -1910,8 +1333,8 @@ "@types/react-dom" ] }, - "@radix-ui/react-tooltip@1.2.4_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==", + "@radix-ui/react-tooltip@1.2.7_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", "dependencies": [ "@radix-ui/primitive", "@radix-ui/react-compose-refs", @@ -1921,8 +1344,8 @@ "@radix-ui/react-popper", "@radix-ui/react-portal", "@radix-ui/react-presence", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "@radix-ui/react-slot@1.2.0_@types+react@19.1.2_react@19.1.0", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", "@radix-ui/react-use-controllable-state", "@radix-ui/react-visually-hidden", "@types/react", @@ -1935,7 +1358,7 @@ "@types/react-dom" ] }, - "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "dependencies": [ "@types/react", @@ -1945,7 +1368,7 @@ "@types/react" ] }, - "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "dependencies": [ "@radix-ui/react-use-effect-event", @@ -1957,7 +1380,7 @@ "@types/react" ] }, - "@radix-ui/react-use-effect-event@0.0.2_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-effect-event@0.0.2_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "dependencies": [ "@radix-ui/react-use-layout-effect", @@ -1968,7 +1391,7 @@ "@types/react" ] }, - "@radix-ui/react-use-escape-keydown@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-escape-keydown@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "dependencies": [ "@radix-ui/react-use-callback-ref", @@ -1979,7 +1402,7 @@ "@types/react" ] }, - "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "dependencies": [ "@types/react", @@ -1989,7 +1412,7 @@ "@types/react" ] }, - "@radix-ui/react-use-previous@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-previous@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "dependencies": [ "@types/react", @@ -1999,7 +1422,7 @@ "@types/react" ] }, - "@radix-ui/react-use-rect@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-rect@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "dependencies": [ "@radix-ui/rect", @@ -2010,7 +1433,7 @@ "@types/react" ] }, - "@radix-ui/react-use-size@1.1.1_@types+react@19.1.2_react@19.1.0": { + "@radix-ui/react-use-size@1.1.1_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "dependencies": [ "@radix-ui/react-use-layout-effect", @@ -2021,10 +1444,10 @@ "@types/react" ] }, - "@radix-ui/react-visually-hidden@1.2.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==", + "@radix-ui/react-visually-hidden@1.2.3_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "dependencies": [ - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "@types/react", "@types/react-dom", "react", @@ -2038,243 +1461,171 @@ "@radix-ui/rect@1.1.1": { "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" }, - "@rollup/plugin-babel@5.3.1_@babel+core@7.27.1_rollup@2.79.2": { - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", - "dependencies": [ - "@babel/core", - "@babel/helper-module-imports", - "@rollup/pluginutils@3.1.0_rollup@2.79.2", - "rollup@2.79.2" - ] - }, - "@rollup/plugin-inject@5.0.5": { - "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", - "dependencies": [ - "@rollup/pluginutils@5.1.4_rollup@2.79.2", - "estree-walker@2.0.2", - "magic-string@0.30.17" - ] - }, - "@rollup/plugin-node-resolve@15.3.1_rollup@2.79.2": { - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", - "dependencies": [ - "@rollup/pluginutils@5.1.4_rollup@2.79.2", - "@types/resolve", - "deepmerge", - "is-module", - "resolve", - "rollup@2.79.2" - ], - "optionalPeers": [ - "rollup@2.79.2" - ] - }, - "@rollup/plugin-replace@2.4.2_rollup@2.79.2": { - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dependencies": [ - "@rollup/pluginutils@3.1.0_rollup@2.79.2", - "magic-string@0.25.9", - "rollup@2.79.2" - ] - }, - "@rollup/plugin-terser@0.4.4_rollup@2.79.2": { - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", - "dependencies": [ - "rollup@2.79.2", - "serialize-javascript", - "smob", - "terser" - ], - "optionalPeers": [ - "rollup@2.79.2" - ] - }, - "@rollup/pluginutils@3.1.0_rollup@2.79.2": { - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dependencies": [ - "@types/estree@0.0.39", - "estree-walker@1.0.1", - "picomatch@2.3.1", - "rollup@2.79.2" - ] + "@rolldown/pluginutils@1.0.0-beta.19": { + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==" }, - "@rollup/pluginutils@5.1.4_rollup@2.79.2": { - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", - "dependencies": [ - "@types/estree@1.0.7", - "estree-walker@2.0.2", - "picomatch@4.0.2", - "rollup@2.79.2" - ], - "optionalPeers": [ - "rollup@2.79.2" - ] - }, - "@rollup/rollup-android-arm-eabi@4.40.1": { - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "@rollup/rollup-android-arm-eabi@4.44.2": { + "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", "os": ["android"], "cpu": ["arm"] }, - "@rollup/rollup-android-arm64@4.40.1": { - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "@rollup/rollup-android-arm64@4.44.2": { + "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", "os": ["android"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-arm64@4.40.1": { - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "@rollup/rollup-darwin-arm64@4.44.2": { + "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rollup/rollup-darwin-x64@4.40.1": { - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "@rollup/rollup-darwin-x64@4.44.2": { + "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", "os": ["darwin"], "cpu": ["x64"] }, - "@rollup/rollup-freebsd-arm64@4.40.1": { - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "@rollup/rollup-freebsd-arm64@4.44.2": { + "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", "os": ["freebsd"], "cpu": ["arm64"] }, - "@rollup/rollup-freebsd-x64@4.40.1": { - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "@rollup/rollup-freebsd-x64@4.44.2": { + "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rollup/rollup-linux-arm-gnueabihf@4.40.1": { - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "@rollup/rollup-linux-arm-gnueabihf@4.44.2": { + "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm-musleabihf@4.40.1": { - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "@rollup/rollup-linux-arm-musleabihf@4.44.2": { + "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", "os": ["linux"], "cpu": ["arm"] }, - "@rollup/rollup-linux-arm64-gnu@4.40.1": { - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "@rollup/rollup-linux-arm64-gnu@4.44.2": { + "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-arm64-musl@4.40.1": { - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "@rollup/rollup-linux-arm64-musl@4.44.2": { + "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", "os": ["linux"], "cpu": ["arm64"] }, - "@rollup/rollup-linux-loongarch64-gnu@4.40.1": { - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "@rollup/rollup-linux-loongarch64-gnu@4.44.2": { + "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", "os": ["linux"], "cpu": ["loong64"] }, - "@rollup/rollup-linux-powerpc64le-gnu@4.40.1": { - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "@rollup/rollup-linux-powerpc64le-gnu@4.44.2": { + "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rollup/rollup-linux-riscv64-gnu@4.40.1": { - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "@rollup/rollup-linux-riscv64-gnu@4.44.2": { + "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-riscv64-musl@4.40.1": { - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "@rollup/rollup-linux-riscv64-musl@4.44.2": { + "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", "os": ["linux"], "cpu": ["riscv64"] }, - "@rollup/rollup-linux-s390x-gnu@4.40.1": { - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "@rollup/rollup-linux-s390x-gnu@4.44.2": { + "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", "os": ["linux"], "cpu": ["s390x"] }, - "@rollup/rollup-linux-x64-gnu@4.40.1": { - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "@rollup/rollup-linux-x64-gnu@4.44.2": { + "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-linux-x64-musl@4.40.1": { - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "@rollup/rollup-linux-x64-musl@4.44.2": { + "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", "os": ["linux"], "cpu": ["x64"] }, - "@rollup/rollup-win32-arm64-msvc@4.40.1": { - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "@rollup/rollup-win32-arm64-msvc@4.44.2": { + "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", "os": ["win32"], "cpu": ["arm64"] }, - "@rollup/rollup-win32-ia32-msvc@4.40.1": { - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "@rollup/rollup-win32-ia32-msvc@4.44.2": { + "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", "os": ["win32"], "cpu": ["ia32"] }, - "@rollup/rollup-win32-x64-msvc@4.40.1": { - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "@rollup/rollup-win32-x64-msvc@4.44.2": { + "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", "os": ["win32"], "cpu": ["x64"] }, - "@surma/rollup-plugin-off-main-thread@2.2.3": { - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", - "dependencies": [ - "ejs", - "json5", - "magic-string@0.25.9", - "string.prototype.matchall" - ] + "@standard-schema/utils@0.3.0": { + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" }, - "@tailwindcss/node@4.1.5": { - "integrity": "sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==", + "@tailwindcss/node@4.1.11": { + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", "dependencies": [ + "@ampproject/remapping", "enhanced-resolve", "jiti", "lightningcss", + "magic-string", + "source-map-js", "tailwindcss" ] }, - "@tailwindcss/oxide-android-arm64@4.1.5": { - "integrity": "sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==", + "@tailwindcss/oxide-android-arm64@4.1.11": { + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", "os": ["android"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-darwin-arm64@4.1.5": { - "integrity": "sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==", + "@tailwindcss/oxide-darwin-arm64@4.1.11": { + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-darwin-x64@4.1.5": { - "integrity": "sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==", + "@tailwindcss/oxide-darwin-x64@4.1.11": { + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", "os": ["darwin"], "cpu": ["x64"] }, - "@tailwindcss/oxide-freebsd-x64@4.1.5": { - "integrity": "sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==", + "@tailwindcss/oxide-freebsd-x64@4.1.11": { + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", "os": ["freebsd"], "cpu": ["x64"] }, - "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5": { - "integrity": "sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11": { + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", "os": ["linux"], "cpu": ["arm"] }, - "@tailwindcss/oxide-linux-arm64-gnu@4.1.5": { - "integrity": "sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==", + "@tailwindcss/oxide-linux-arm64-gnu@4.1.11": { + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-linux-arm64-musl@4.1.5": { - "integrity": "sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==", + "@tailwindcss/oxide-linux-arm64-musl@4.1.11": { + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-linux-x64-gnu@4.1.5": { - "integrity": "sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==", + "@tailwindcss/oxide-linux-x64-gnu@4.1.11": { + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", "os": ["linux"], "cpu": ["x64"] }, - "@tailwindcss/oxide-linux-x64-musl@4.1.5": { - "integrity": "sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==", + "@tailwindcss/oxide-linux-x64-musl@4.1.11": { + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", "os": ["linux"], "cpu": ["x64"] }, - "@tailwindcss/oxide-wasm32-wasi@4.1.5": { - "integrity": "sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==", + "@tailwindcss/oxide-wasm32-wasi@4.1.11": { + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", "dependencies": [ "@emnapi/core", "@emnapi/runtime", @@ -2285,18 +1636,22 @@ ], "cpu": ["wasm32"] }, - "@tailwindcss/oxide-win32-arm64-msvc@4.1.5": { - "integrity": "sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==", + "@tailwindcss/oxide-win32-arm64-msvc@4.1.11": { + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", "os": ["win32"], "cpu": ["arm64"] }, - "@tailwindcss/oxide-win32-x64-msvc@4.1.5": { - "integrity": "sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==", + "@tailwindcss/oxide-win32-x64-msvc@4.1.11": { + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", "os": ["win32"], "cpu": ["x64"] }, - "@tailwindcss/oxide@4.1.5": { - "integrity": "sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==", + "@tailwindcss/oxide@4.1.11": { + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "dependencies": [ + "detect-libc", + "tar" + ], "optionalDependencies": [ "@tailwindcss/oxide-android-arm64", "@tailwindcss/oxide-darwin-arm64", @@ -2310,56 +1665,47 @@ "@tailwindcss/oxide-wasm32-wasi", "@tailwindcss/oxide-win32-arm64-msvc", "@tailwindcss/oxide-win32-x64-msvc" - ] + ], + "scripts": true }, - "@tailwindcss/postcss@4.1.5": { - "integrity": "sha512-5lAC2/pzuyfhsFgk6I58HcNy6vPK3dV/PoPxSDuOTVbDvCddYHzHiJZZInGIY0venvzzfrTEUAXJFULAfFmObg==", + "@tailwindcss/postcss@4.1.11": { + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", "dependencies": [ "@alloc/quick-lru", "@tailwindcss/node", - "@tailwindcss/oxide", - "postcss", - "tailwindcss" - ] - }, - "@tanstack/history@1.115.0": { - "integrity": "sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==" - }, - "@tanstack/react-router-devtools@1.120.15_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0_solid-js@1.9.7__seroval@1.3.2": { - "integrity": "sha512-5KcUXc3fkiLo/6Y56gOM3JqmYXG1ElIH2iyUWuG5IlcegLrpXhu4OBQ+8Q4+62CD0OKy0ifUDyemrCOAEOfCvw==", - "dependencies": [ - "@tanstack/react-router", - "@tanstack/router-devtools-core", - "react", - "react-dom", - "solid-js" + "@tailwindcss/oxide", + "postcss", + "tailwindcss" ] }, - "@tanstack/react-router-devtools@1.120.16_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0_solid-js@1.9.7__seroval@1.3.2": { - "integrity": "sha512-DWXmMLknVJJMGP2k5yeUWBDhJOHbV2jVfnZKxtGzA64xXhwDFgU9qpodcmYSq3+kHWsKrd7iX0wc7d27rGwGDA==", + "@tanstack/history@1.121.34": { + "integrity": "sha512-YL8dGi5ZU+xvtav2boRlw4zrRghkY6hvdcmHhA0RGSJ/CBgzv+cbADW9eYJLx74XMZvIQ1pp6VMbrpXnnM5gHA==" + }, + "@tanstack/react-router-devtools@1.125.6_@tanstack+react-router@1.125.6__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-3+Np/HPQ1jpdai58xY6fScnjJz08iIG6aKhCVVy8tfPC76jTyXc+ygqM9wtUt3kJYy9/Lf7j3dAJlbtxF+TIXg==", "dependencies": [ "@tanstack/react-router", "@tanstack/router-devtools-core", "react", - "react-dom", - "solid-js" + "react-dom" ] }, - "@tanstack/react-router@1.120.15_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-apzBmXh4pHwqUGU3kD8y2FJMi7rVoUbRxh5oV7v8kEb6Aq5Xpdo+OcpThw8h/M2zv7v4Ef8IoY6WFCKKu3HBjQ==", + "@tanstack/react-router@1.125.6_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-znyUGTq+WRhXwToNTYiluUBLjMdQVxz+ZQ9Ep2PBuS9O+3Qm3kaM7n64hA84ISoCtLqMwTo7Ofw0W4WeLdjpYg==", "dependencies": [ "@tanstack/history", "@tanstack/react-store", "@tanstack/router-core", - "jsesc@3.1.0", + "isbot", + "jsesc", "react", "react-dom", "tiny-invariant", "tiny-warning" ] }, - "@tanstack/react-store@0.7.1_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==", + "@tanstack/react-store@0.7.3_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==", "dependencies": [ "@tanstack/store", "react", @@ -2367,16 +1713,28 @@ "use-sync-external-store" ] }, - "@tanstack/router-core@1.120.15": { - "integrity": "sha512-soLj+mEuvSxAVFK/3b85IowkkvmSuQL6J0RSIyKJFGFgy0CmUzpcBGEO99+JNWvvvzHgIoY4F4KtLIN+rvFSFA==", + "@tanstack/router-cli@1.125.4": { + "integrity": "sha512-09DGnwDKsRdLmbAwstN2biJ7P37PeD/Fv2uDDIOfc+AlBw6gfq8byy0zIYFXmQCjSBmHwmS9A4XOmfzaDkfQng==", + "dependencies": [ + "@tanstack/router-generator", + "chokidar", + "yargs" + ], + "bin": true + }, + "@tanstack/router-core@1.125.4": { + "integrity": "sha512-tdgGI0Kwt3Lgs9ceLbG32NPh4I2H1T9t2TKjcS+I78sifm5rjTWV8lfqVRNrvMPk5ek60vXPOL2AHAUg6ohwsA==", "dependencies": [ "@tanstack/history", "@tanstack/store", - "tiny-invariant" + "cookie-es", + "jsesc", + "tiny-invariant", + "tiny-warning" ] }, - "@tanstack/router-devtools-core@1.120.15_@tanstack+router-core@1.120.15_solid-js@1.9.7__seroval@1.3.2_tiny-invariant@1.3.3": { - "integrity": "sha512-AT9obPHKpJqnHMbwshozSy6sApg5LchiAll3blpS3MMDybUCidYHrdhe9MZJLmlC99IQiEGmuZERP3VRcuPNHg==", + "@tanstack/router-devtools-core@1.125.4_@tanstack+router-core@1.125.4_solid-js@1.9.7__seroval@1.3.2_tiny-invariant@1.3.3": { + "integrity": "sha512-5QbCQCcJcN/M0NF2TARKqauJ8QeRuk7kyHQMCqOoSJWWGUVcDHEmcbg1ZCJevfPVZPgnUjV9mqDfCPTYWT8/+w==", "dependencies": [ "@tanstack/router-core", "clsx", @@ -2385,32 +1743,32 @@ "tiny-invariant" ] }, - "@tanstack/router-devtools@1.120.15_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-u7KbvupWSppoEUYuhCBzmWkd1hcODzHhvGIuWZKoQO9q/qeNY5XptbzGqBSUooXyoF4T/pAdCRILF5zFIqexJw==", + "@tanstack/router-devtools@1.125.6_@tanstack+react-router@1.125.6__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "integrity": "sha512-Orlf0+5wANehVq5ocxfe3YyJnEtvc5tcNN/ldX20fOKyYeIhVQzR8Oa6RO3Jk2sY31grMwi6q199MTAJ7XshLQ==", "dependencies": [ "@tanstack/react-router", - "@tanstack/react-router-devtools@1.120.15_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0_solid-js@1.9.7__seroval@1.3.2", + "@tanstack/react-router-devtools", "clsx", "goober", "react", "react-dom" ] }, - "@tanstack/router-generator@1.120.15_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0": { - "integrity": "sha512-QwZ0rNXxzgOEUDRRAEWVjofKxuxSMIYEdYC3z20k6a7jkLC6pnlCORFx41Vf4xVCO6eElqlrUKXWLTleYSsvQw==", + "@tanstack/router-generator@1.125.4": { + "integrity": "sha512-jF71znMvpZxmkQF0MxfjKKyvXtft9NWRCVcLhb+6d/8nrVGNiEw+dsXn/CLpeRQLk7Mg/fsp/WipBql1dd3Qaw==", "dependencies": [ - "@tanstack/react-router", + "@tanstack/router-core", + "@tanstack/router-utils", "@tanstack/virtual-file-routes", "prettier", + "recast", + "source-map@0.7.4", "tsx", "zod" - ], - "optionalPeers": [ - "@tanstack/react-router" ] }, - "@tanstack/router-plugin@1.120.15_@tanstack+react-router@1.120.15__react@19.1.0__react-dom@19.1.0___react@19.1.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@babel+core@7.27.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+node@22.15.3": { - "integrity": "sha512-ARuuPRKO5HzN3V0LzmkIGm0e447t5VCy2ijbUnzd08KjcJm3lG221ViC2qI+vTom1zp6yeNZHfJW1LBh1yLrTw==", + "@tanstack/router-plugin@1.125.6_@tanstack+react-router@1.125.6__react@19.1.0__react-dom@19.1.0___react@19.1.0_vite@7.0.2__@types+node@22.16.0__picomatch@4.0.2_@babel+core@7.28.0_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+node@22.16.0": { + "integrity": "sha512-SWfp++tkjb0grVqa/Xdvi9QAs93e9/fZMBZ6q0fvvQruMyciCmjWyE/qV3tS/Qh0WZdzIRP6yl8Gha2Lin4q1w==", "dependencies": [ "@babel/core", "@babel/plugin-syntax-jsx", @@ -2423,9 +1781,6 @@ "@tanstack/router-generator", "@tanstack/router-utils", "@tanstack/virtual-file-routes", - "@types/babel__core", - "@types/babel__template", - "@types/babel__traverse", "babel-dead-code-elimination", "chokidar", "unplugin", @@ -2437,20 +1792,22 @@ "vite" ] }, - "@tanstack/router-utils@1.115.0": { - "integrity": "sha512-Dng4y+uLR9b5zPGg7dHReHOTHQa6x+G6nCoZshsDtWrYsrdCcJEtLyhwZ5wG8OyYS6dVr/Cn+E5Bd2b6BhJ89w==", + "@tanstack/router-utils@1.121.21_@babel+core@7.28.0": { + "integrity": "sha512-u7ubq1xPBtNiU7Fm+EOWlVWdgFLzuKOa1thhqdscVn8R4dNMUd1VoOjZ6AKmLw201VaUhFtlX+u0pjzI6szX7A==", "dependencies": [ + "@babel/core", "@babel/generator", "@babel/parser", + "@babel/preset-typescript", "ansis", "diff" ] }, - "@tanstack/store@0.7.1": { - "integrity": "sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==" + "@tanstack/store@0.7.2": { + "integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==" }, - "@tanstack/virtual-file-routes@1.115.0": { - "integrity": "sha512-XLUh1Py3AftcERrxkxC5Y5m5mfllRH3YR6YVlyjFgI2Tc2Ssy2NKmQFQIafoxfW459UJ8Dn81nWKETEIJifE4g==" + "@tanstack/virtual-file-routes@1.121.21": { + "integrity": "sha512-3nuYsTyaq6ZN7jRZ9z6Gj3GXZqBOqOT0yzd/WZ33ZFfv4yVNIvsa5Lw+M1j3sgyEAxKMqGu/FaNi7FCjr3yOdw==" }, "@testing-library/dom@10.4.0": { "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", @@ -2477,7 +1834,7 @@ "redent" ] }, - "@testing-library/react@16.3.0_@testing-library+dom@10.4.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@testing-library/react@16.3.0_@testing-library+dom@10.4.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dependencies": [ "@babel/runtime", @@ -3876,8 +3233,14 @@ "@babel/types" ] }, - "@types/chrome@0.0.318": { - "integrity": "sha512-rrtyYQ1t+g7EyG0FejE+UXQBjSGUHGh0RIdXwUT/laPo9T724NOIgXA94ns6ewmNauwijYa5ck3+dBxWnHcynQ==", + "@types/chai@5.2.2": { + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dependencies": [ + "@types/deep-eql" + ] + }, + "@types/chrome@0.0.329": { + "integrity": "sha512-jAZX4QMnAa1bTSWoQ5/EhhiY1t+1B7a5ZCY5ZEs61tWiQfxXAkfBSxCkQWhGxJoiq/4b4vtcmYEebNEmlLKecg==", "dependencies": [ "@types/filesystem", "@types/har-format" @@ -3886,11 +3249,11 @@ "@types/d3-voronoi@1.1.12": { "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==" }, - "@types/estree@0.0.39": { - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + "@types/deep-eql@4.0.2": { + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==" }, - "@types/estree@1.0.7": { - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "@types/filesystem@0.0.36": { "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", @@ -3927,32 +3290,41 @@ "@types/pbf" ] }, - "@types/node@22.15.3": { - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "@types/node@20.19.4": { + "integrity": "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==", + "dependencies": [ + "undici-types@6.21.0" + ] + }, + "@types/node@22.16.0": { + "integrity": "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==", + "dependencies": [ + "undici-types@6.21.0" + ] + }, + "@types/node@24.0.10": { + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", "dependencies": [ - "undici-types" + "undici-types@7.8.0" ] }, "@types/pbf@3.0.5": { "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==" }, - "@types/react-dom@19.1.3_@types+react@19.1.2": { - "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", + "@types/react-dom@19.1.6_@types+react@19.1.8": { + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dependencies": [ "@types/react" ] }, - "@types/react@19.1.2": { - "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "@types/react@19.1.8": { + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dependencies": [ "csstype" ] }, - "@types/resolve@1.20.2": { - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" - }, - "@types/serviceworker@0.0.133": { - "integrity": "sha512-lEyAbLUMztFbps2GVZ5mKIXl5+BZiGfOOA8JxN6KTiT91Ct31lSAHISKUl2+iOwmrUwNvWeI9rbsFxFqDZCghQ==" + "@types/serviceworker@0.0.140": { + "integrity": "sha512-9YBn/87nAamFtRCKEFZh2F+3Hs0UQzrxPgDbiqTgIS+F0LxbcVavgKtLhqSgSVF+OYtQM+fw8r3VXs7P1QpiXg==" }, "@types/supercluster@7.1.3": { "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", @@ -3960,15 +3332,18 @@ "@types/geojson" ] }, - "@types/trusted-types@2.0.7": { - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" - }, "@types/w3c-web-serial@1.0.8": { "integrity": "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A==" }, + "@types/web-bluetooth@0.0.20": { + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, "@types/web-bluetooth@0.0.21": { "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" }, + "@types/whatwg-mimetype@3.0.2": { + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==" + }, "@vis.gl/react-mapbox@8.0.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-NFk0vsWcNzSs0YCsVdt2100Zli9QWR+pje8DacpLkkGEAXFaJsFtI1oKD0Hatiate4/iAIW39SQHhgfhbeEPfQ==", "dependencies": [ @@ -3976,7 +3351,7 @@ "react-dom" ] }, - "@vis.gl/react-maplibre@8.0.4_maplibre-gl@5.4.0_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "@vis.gl/react-maplibre@8.0.4_maplibre-gl@5.6.1_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-HwZyfLjEu+y1mUFvwDAkVxinGm8fEegaWN+O8np/WZ2Sqe5Lv6OXFpV6GWz9LOEvBYMbGuGk1FQdejo+4HCJ5w==", "dependencies": [ "@maplibre/maplibre-gl-style-spec@19.3.3", @@ -3988,86 +3363,80 @@ "maplibre-gl" ] }, - "@vitejs/plugin-react@4.4.1_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@babel+core@7.27.1_@types+node@22.15.3": { - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "@vitejs/plugin-react@4.6.0_vite@7.0.2__@types+node@22.16.0__picomatch@4.0.2_@babel+core@7.28.0_@types+node@22.16.0": { + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", "dependencies": [ "@babel/core", "@babel/plugin-transform-react-jsx-self", "@babel/plugin-transform-react-jsx-source", + "@rolldown/pluginutils", "@types/babel__core", "react-refresh", "vite" ] }, - "@vitest/expect@3.1.2": { - "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", + "@vitest/expect@3.2.4": { + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dependencies": [ + "@types/chai", "@vitest/spy", "@vitest/utils", "chai", "tinyrainbow" ] }, - "@vitest/mocker@3.1.2_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@types+node@22.15.3": { - "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", + "@vitest/mocker@3.2.4_vite@7.0.2__@types+node@22.16.0__picomatch@4.0.2_@types+node@22.16.0": { + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dependencies": [ "@vitest/spy", - "estree-walker@3.0.3", - "magic-string@0.30.17", + "estree-walker", + "magic-string", "vite" ], "optionalPeers": [ "vite" ] }, - "@vitest/pretty-format@3.1.2": { - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", + "@vitest/pretty-format@3.2.4": { + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dependencies": [ "tinyrainbow" ] }, - "@vitest/runner@3.1.2": { - "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", + "@vitest/runner@3.2.4": { + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dependencies": [ "@vitest/utils", - "pathe" + "pathe", + "strip-literal" ] }, - "@vitest/snapshot@3.1.2": { - "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", + "@vitest/snapshot@3.2.4": { + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dependencies": [ "@vitest/pretty-format", - "magic-string@0.30.17", + "magic-string", "pathe" ] }, - "@vitest/spy@3.1.2": { - "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", + "@vitest/spy@3.2.4": { + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dependencies": [ "tinyspy" ] }, - "@vitest/utils@3.1.2": { - "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", + "@vitest/utils@3.2.4": { + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dependencies": [ "@vitest/pretty-format", "loupe", "tinyrainbow" ] }, - "acorn@8.14.1": { - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "acorn@8.15.0": { + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "bin": true }, - "ajv@8.17.1": { - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": [ - "fast-deep-equal", - "fast-uri", - "json-schema-traverse", - "require-from-string" - ] - }, "ansi-regex@5.0.1": { "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, @@ -4080,8 +3449,8 @@ "ansi-styles@5.2.0": { "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" }, - "ansis@3.17.0": { - "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==" + "ansis@4.1.0": { + "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==" }, "anymatch@3.1.3": { "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", @@ -4090,11 +3459,8 @@ "picomatch@2.3.1" ] }, - "argparse@2.0.1": { - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "aria-hidden@1.2.4": { - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "aria-hidden@1.2.6": { + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "dependencies": [ "tslib@2.8.1" ] @@ -4111,59 +3477,19 @@ "arr-union@3.1.0": { "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==" }, - "array-buffer-byte-length@1.0.2": { - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dependencies": [ - "call-bound", - "is-array-buffer" - ] - }, - "arraybuffer.prototype.slice@1.0.4": { - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dependencies": [ - "array-buffer-byte-length", - "call-bind", - "define-properties", - "es-abstract", - "es-errors", - "get-intrinsic", - "is-array-buffer" - ] - }, - "asn1.js@4.10.1": { - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dependencies": [ - "bn.js@4.12.2", - "inherits", - "minimalistic-assert" - ] - }, - "assert@2.1.0": { - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dependencies": [ - "call-bind", - "is-nan", - "object-is", - "object.assign", - "util" - ] - }, "assertion-error@2.0.1": { "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==" }, "assign-symbols@1.0.0": { "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==" }, - "async-function@1.0.0": { - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==" - }, - "async@3.2.6": { - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "at-least-node@1.0.0": { - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + "ast-types@0.16.1": { + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dependencies": [ + "tslib@2.8.1" + ] }, - "autoprefixer@10.4.21_postcss@8.5.3": { + "autoprefixer@10.4.21_postcss@8.5.6": { "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "dependencies": [ "browserslist", @@ -4176,12 +3502,6 @@ ], "bin": true }, - "available-typed-arrays@1.0.7": { - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dependencies": [ - "possible-typed-array-names" - ] - }, "babel-dead-code-elimination@1.0.10": { "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", "dependencies": [ @@ -4191,33 +3511,6 @@ "@babel/types" ] }, - "babel-plugin-polyfill-corejs2@0.4.13_@babel+core@7.27.1": { - "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", - "dependencies": [ - "@babel/compat-data", - "@babel/core", - "@babel/helper-define-polyfill-provider", - "semver" - ] - }, - "babel-plugin-polyfill-corejs3@0.11.1_@babel+core@7.27.1": { - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", - "dependencies": [ - "@babel/core", - "@babel/helper-define-polyfill-provider", - "core-js-compat" - ] - }, - "babel-plugin-polyfill-regenerator@0.6.4_@babel+core@7.27.1": { - "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", - "dependencies": [ - "@babel/core", - "@babel/helper-define-polyfill-provider" - ] - }, - "balanced-match@1.0.2": { - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, "base64-js@1.5.1": { "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, @@ -4227,99 +3520,14 @@ "binary-extensions@2.3.0": { "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" }, - "bn.js@4.12.2": { - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" - }, - "bn.js@5.2.2": { - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==" - }, - "brace-expansion@1.1.11": { - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": [ - "balanced-match", - "concat-map" - ] - }, - "brace-expansion@2.0.1": { - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": [ - "balanced-match" - ] - }, "braces@3.0.3": { "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": [ "fill-range" ] }, - "brorand@1.1.0": { - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, - "browser-resolve@2.0.0": { - "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", - "dependencies": [ - "resolve" - ] - }, - "browserify-aes@1.2.0": { - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dependencies": [ - "buffer-xor", - "cipher-base", - "create-hash", - "evp_bytestokey", - "inherits", - "safe-buffer@5.2.1" - ] - }, - "browserify-cipher@1.0.1": { - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dependencies": [ - "browserify-aes", - "browserify-des", - "evp_bytestokey" - ] - }, - "browserify-des@1.0.2": { - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dependencies": [ - "cipher-base", - "des.js", - "inherits", - "safe-buffer@5.2.1" - ] - }, - "browserify-rsa@4.1.1": { - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "dependencies": [ - "bn.js@5.2.2", - "randombytes", - "safe-buffer@5.2.1" - ] - }, - "browserify-sign@4.2.3": { - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", - "dependencies": [ - "bn.js@5.2.2", - "browserify-rsa", - "create-hash", - "create-hmac", - "elliptic", - "hash-base", - "inherits", - "parse-asn1", - "readable-stream@2.3.8", - "safe-buffer@5.2.1" - ] - }, - "browserify-zlib@0.2.0": { - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dependencies": [ - "pako" - ] - }, - "browserslist@4.24.5": { - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "browserslist@4.25.1": { + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dependencies": [ "caniuse-lite", "electron-to-chromium", @@ -4331,26 +3539,6 @@ "buffer-from@1.1.2": { "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, - "buffer-xor@1.0.3": { - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" - }, - "buffer@5.7.1": { - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dependencies": [ - "base64-js", - "ieee754" - ] - }, - "builtin-status-codes@3.0.0": { - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" - }, - "bundle-require@5.1.0_esbuild@0.25.3": { - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dependencies": [ - "esbuild", - "load-tsconfig" - ] - }, "bytewise-core@1.2.3": { "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", "dependencies": [ @@ -4367,31 +3555,8 @@ "cac@6.7.14": { "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" }, - "call-bind-apply-helpers@1.0.2": { - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": [ - "es-errors", - "function-bind" - ] - }, - "call-bind@1.0.8": { - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "get-intrinsic", - "set-function-length" - ] - }, - "call-bound@1.0.4": { - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": [ - "call-bind-apply-helpers", - "get-intrinsic" - ] - }, - "caniuse-lite@1.0.30001716": { - "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==" + "caniuse-lite@1.0.30001727": { + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==" }, "chai@5.2.0": { "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", @@ -4438,29 +3603,30 @@ "chownr@3.0.0": { "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" }, - "cipher-base@1.0.6": { - "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", - "dependencies": [ - "inherits", - "safe-buffer@5.2.1" - ] - }, "class-variance-authority@0.7.1": { "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "dependencies": [ "clsx" ] }, + "cliui@8.0.1": { + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": [ + "string-width", + "strip-ansi", + "wrap-ansi" + ] + }, "clsx@2.1.1": { "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" }, - "cmdk@1.1.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2": { + "cmdk@1.1.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8": { "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", "dependencies": [ "@radix-ui/react-compose-refs", "@radix-ui/react-dialog", "@radix-ui/react-id", - "@radix-ui/react-primitive@2.1.0_@types+react@19.1.2_@types+react-dom@19.1.3__@types+react@19.1.2_react@19.1.0_react-dom@19.1.0__react@19.1.0", + "@radix-ui/react-primitive", "react", "react-dom" ] @@ -4480,12 +3646,6 @@ "commander@2.20.3": { "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "common-tags@1.8.2": { - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" - }, - "concat-map@0.0.1": { - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, "concaveman@1.2.1": { "integrity": "sha512-PwZYKaM/ckQSa8peP5JpVr7IMJ4Nn/MHIaWUjP4be+KoZ7Botgs8seAZGpmaOM+UZXawcdYRao/px9ycrCihHw==", "dependencies": [ @@ -4495,23 +3655,11 @@ "tinyqueue@2.0.3" ] }, - "console-browserify@1.2.0": { - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" - }, - "constants-browserify@1.0.0": { - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" - }, "convert-source-map@2.0.0": { "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, - "cookie@1.0.2": { - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" - }, - "core-js-compat@3.42.0": { - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", - "dependencies": [ - "browserslist" - ] + "cookie-es@1.2.2": { + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==" }, "core-util-is@1.0.3": { "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" @@ -4519,67 +3667,16 @@ "crc@4.3.2": { "integrity": "sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==" }, - "create-ecdh@4.0.4": { - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dependencies": [ - "bn.js@4.12.2", - "elliptic" - ] - }, - "create-hash@1.2.0": { - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dependencies": [ - "cipher-base", - "inherits", - "md5.js", - "ripemd160", - "sha.js" - ] - }, - "create-hmac@1.1.7": { - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dependencies": [ - "cipher-base", - "create-hash", - "inherits", - "ripemd160", - "safe-buffer@5.2.1", - "sha.js" - ] - }, - "create-require@1.1.1": { - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, "cross-fetch@4.0.0": { "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", "dependencies": [ "node-fetch" ] }, - "crypto-browserify@3.12.1": { - "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "dependencies": [ - "browserify-cipher", - "browserify-sign", - "create-ecdh", - "create-hash", - "create-hmac", - "diffie-hellman", - "hash-base", - "inherits", - "pbkdf2", - "public-encrypt", - "randombytes", - "randomfill" - ] - }, - "crypto-random-string@2.0.0": { - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" - }, "crypto-random-string@5.0.0": { "integrity": "sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==", "dependencies": [ - "type-fest@2.19.0" + "type-fest" ] }, "css.escape@1.5.1": { @@ -4600,32 +3697,8 @@ "d3-voronoi@1.1.2": { "integrity": "sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw==" }, - "data-view-buffer@1.0.2": { - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dependencies": [ - "call-bound", - "es-errors", - "is-data-view" - ] - }, - "data-view-byte-length@1.0.2": { - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dependencies": [ - "call-bound", - "es-errors", - "is-data-view" - ] - }, - "data-view-byte-offset@1.0.1": { - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dependencies": [ - "call-bound", - "es-errors", - "is-data-view" - ] - }, - "debug@4.4.0": { - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "debug@4.4.1": { + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dependencies": [ "ms" ] @@ -4633,54 +3706,17 @@ "deep-eql@5.0.2": { "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==" }, - "deep-object-diff@1.1.9": { - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==" - }, - "deepmerge@4.3.1": { - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" - }, - "define-data-property@1.1.4": { - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": [ - "es-define-property", - "es-errors", - "gopd" - ] - }, - "define-properties@1.2.1": { - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": [ - "define-data-property", - "has-property-descriptors", - "object-keys" - ] - }, "dequal@2.0.3": { "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, - "des.js@1.1.0": { - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dependencies": [ - "inherits", - "minimalistic-assert" - ] - }, "detect-libc@2.0.4": { "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==" }, "detect-node-es@1.1.0": { "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, - "diff@7.0.0": { - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==" - }, - "diffie-hellman@5.0.3": { - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dependencies": [ - "bn.js@4.12.2", - "miller-rabin", - "randombytes" - ] + "diff@8.0.2": { + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==" }, "dom-accessibility-api@0.5.16": { "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" @@ -4688,17 +3724,6 @@ "dom-accessibility-api@0.6.3": { "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==" }, - "domain-browser@4.22.0": { - "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==" - }, - "dunder-proto@1.0.1": { - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": [ - "call-bind-apply-helpers", - "es-errors", - "gopd" - ] - }, "duplex-maker@1.0.0": { "integrity": "sha512-KoHuzggxg7f+vvjqOHfXxaQYI1POzBm+ah0eec7YDssZmbt6QFBI8d1nl5GQwAgR2f+VQCPvyvZtmWWqWuFtlA==" }, @@ -4717,131 +3742,30 @@ "earcut@3.0.1": { "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==" }, - "ejs@3.1.10": { - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": [ - "jake" - ], - "bin": true - }, - "electron-to-chromium@1.5.149": { - "integrity": "sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ==" + "electron-to-chromium@1.5.180": { + "integrity": "sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==" }, - "elliptic@6.6.1": { - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "dependencies": [ - "bn.js@4.12.2", - "brorand", - "hash.js", - "hmac-drbg", - "inherits", - "minimalistic-assert", - "minimalistic-crypto-utils" - ] + "emoji-regex@8.0.0": { + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "end-of-stream@1.4.4": { - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "end-of-stream@1.4.5": { + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dependencies": [ "once" ] }, - "enhanced-resolve@5.18.1": { - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "enhanced-resolve@5.18.2": { + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", "dependencies": [ "graceful-fs", "tapable" ] }, - "es-abstract@1.23.9": { - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dependencies": [ - "array-buffer-byte-length", - "arraybuffer.prototype.slice", - "available-typed-arrays", - "call-bind", - "call-bound", - "data-view-buffer", - "data-view-byte-length", - "data-view-byte-offset", - "es-define-property", - "es-errors", - "es-object-atoms", - "es-set-tostringtag", - "es-to-primitive", - "function.prototype.name", - "get-intrinsic", - "get-proto", - "get-symbol-description", - "globalthis", - "gopd", - "has-property-descriptors", - "has-proto", - "has-symbols", - "hasown", - "internal-slot", - "is-array-buffer", - "is-callable", - "is-data-view", - "is-regex", - "is-shared-array-buffer", - "is-string", - "is-typed-array", - "is-weakref", - "math-intrinsics", - "object-inspect", - "object-keys", - "object.assign", - "own-keys", - "regexp.prototype.flags", - "safe-array-concat", - "safe-push-apply", - "safe-regex-test", - "set-proto", - "string.prototype.trim", - "string.prototype.trimend", - "string.prototype.trimstart", - "typed-array-buffer", - "typed-array-byte-length", - "typed-array-byte-offset", - "typed-array-length", - "unbox-primitive", - "which-typed-array" - ] - }, - "es-define-property@1.0.1": { - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors@1.3.0": { - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, "es-module-lexer@1.7.0": { "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==" }, - "es-object-atoms@1.1.1": { - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": [ - "es-errors" - ] - }, - "es-set-tostringtag@2.1.0": { - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dependencies": [ - "es-errors", - "get-intrinsic", - "has-tostringtag", - "hasown" - ] - }, - "es-to-primitive@1.3.0": { - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dependencies": [ - "is-callable", - "is-date-object", - "is-symbol" - ] - }, - "esbuild@0.25.3": { - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "esbuild@0.25.6": { + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "optionalDependencies": [ "@esbuild/aix-ppc64", "@esbuild/android-arm", @@ -4864,6 +3788,7 @@ "@esbuild/netbsd-x64", "@esbuild/openbsd-arm64", "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", "@esbuild/sunos-x64", "@esbuild/win32-arm64", "@esbuild/win32-ia32", @@ -4875,33 +3800,18 @@ "escalade@3.2.0": { "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, - "estree-walker@1.0.1": { - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" - }, - "estree-walker@2.0.2": { - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + "esprima@4.0.1": { + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": true }, "estree-walker@3.0.3": { "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dependencies": [ - "@types/estree@1.0.7" - ] - }, - "esutils@2.0.3": { - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "events@3.3.0": { - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "evp_bytestokey@1.0.3": { - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dependencies": [ - "md5.js", - "safe-buffer@5.2.1" + "@types/estree" ] }, - "expect-type@1.2.1": { - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==" + "expect-type@1.2.2": { + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==" }, "extend-shallow@2.0.1": { "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", @@ -4919,30 +3829,8 @@ "fast-deep-equal@3.1.3": { "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "fast-glob@3.3.3": { - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dependencies": [ - "@nodelib/fs.stat", - "@nodelib/fs.walk", - "glob-parent", - "merge2", - "micromatch" - ] - }, - "fast-json-stable-stringify@2.1.0": { - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-uri@3.0.6": { - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==" - }, - "fastq@1.19.1": { - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dependencies": [ - "reusify" - ] - }, - "fdir@6.4.4_picomatch@4.0.2": { - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "fdir@6.4.6_picomatch@4.0.2": { + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dependencies": [ "picomatch@4.0.2" ], @@ -4950,83 +3838,19 @@ "picomatch@4.0.2" ] }, - "filelist@1.0.4": { - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": [ - "minimatch@5.1.6" - ] - }, "fill-range@7.1.1": { "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": [ "to-regex-range" ] }, - "find-up@5.0.0": { - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": [ - "locate-path@6.0.0", - "path-exists@4.0.0" - ] - }, - "find-up@7.0.0": { - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dependencies": [ - "locate-path@7.2.0", - "path-exists@5.0.0", - "unicorn-magic" - ] - }, - "for-each@0.3.5": { - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dependencies": [ - "is-callable" - ] - }, "fraction.js@4.3.7": { "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" }, - "fs-extra@11.3.0": { - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dependencies": [ - "graceful-fs", - "jsonfile", - "universalify" - ] - }, - "fs-extra@9.1.0": { - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": [ - "at-least-node", - "graceful-fs", - "jsonfile", - "universalify" - ] - }, - "fs.realpath@1.0.0": { - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "fsevents@2.3.3": { "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "os": ["darwin"], - "scripts": true - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "function.prototype.name@1.1.8": { - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dependencies": [ - "call-bind", - "call-bound", - "define-properties", - "functions-have-names", - "hasown", - "is-callable" - ] - }, - "functions-have-names@1.2.3": { - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + "scripts": true }, "gensync@1.0.0-beta.2": { "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" @@ -5046,45 +3870,15 @@ "geojson-vt@4.0.2": { "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==" }, - "get-intrinsic@1.3.0": { - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "es-errors", - "es-object-atoms", - "function-bind", - "get-proto", - "gopd", - "has-symbols", - "hasown", - "math-intrinsics" - ] + "get-caller-file@2.0.5": { + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-nonce@1.0.1": { "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" }, - "get-own-enumerable-property-symbols@3.0.2": { - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" - }, - "get-proto@1.0.1": { - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": [ - "dunder-proto", - "es-object-atoms" - ] - }, "get-stream@6.0.1": { "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" }, - "get-symbol-description@1.1.0": { - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic" - ] - }, "get-tsconfig@4.10.1": { "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "dependencies": [ @@ -5103,18 +3897,6 @@ "is-glob" ] }, - "glob@7.2.3": { - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": [ - "fs.realpath", - "inflight", - "inherits", - "minimatch@3.1.2", - "once", - "path-is-absolute" - ], - "deprecated": true - }, "global-prefix@4.0.0": { "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", "dependencies": [ @@ -5123,25 +3905,12 @@ "which" ] }, - "globals@11.12.0": { - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "globalthis@1.0.4": { - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dependencies": [ - "define-properties", - "gopd" - ] - }, "goober@2.1.16_csstype@3.1.3": { "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", "dependencies": [ "csstype" ] }, - "gopd@1.2.0": { - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, "graceful-fs@4.2.11": { "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, @@ -5154,79 +3923,25 @@ ], "bin": true }, - "happy-dom@17.4.6": { - "integrity": "sha512-OEV1hDe9i2rFr66+WZNiwy1S8rAJy6bRXmXql68YJDjdfHBRbN76om+qVh68vQACf6y5Bcr90e/oK53RQxsDdg==", + "happy-dom@18.0.1": { + "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==", "dependencies": [ - "webidl-conversions@7.0.0", + "@types/node@20.19.4", + "@types/whatwg-mimetype", "whatwg-mimetype" ] }, - "has-bigints@1.1.0": { - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==" - }, "has-flag@4.0.0": { "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "has-property-descriptors@1.0.2": { - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": [ - "es-define-property" - ] - }, - "has-proto@1.2.0": { - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dependencies": [ - "dunder-proto" - ] - }, - "has-symbols@1.1.0": { - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "has-tostringtag@1.0.2": { - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": [ - "has-symbols" - ] - }, - "hash-base@3.0.5": { - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dependencies": [ - "inherits", - "safe-buffer@5.2.1" - ] - }, - "hash.js@1.1.7": { - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": [ - "inherits", - "minimalistic-assert" - ] - }, - "hasown@2.0.2": { - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": [ - "function-bind" - ] - }, - "hmac-drbg@1.0.1": { - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dependencies": [ - "hash.js", - "minimalistic-assert", - "minimalistic-crypto-utils" - ] - }, "html-parse-stringify@3.0.1": { "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", "dependencies": [ "void-elements" ] }, - "https-browserify@1.0.0": { - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" - }, - "i18next-browser-languagedetector@8.1.0": { - "integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==", + "i18next-browser-languagedetector@8.2.0": { + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", "dependencies": [ "@babel/runtime" ] @@ -5237,8 +3952,8 @@ "cross-fetch" ] }, - "i18next@25.2.0_typescript@5.8.3": { - "integrity": "sha512-ERhJICsxkw1vE7G0lhCUYv4ZxdBEs03qblt1myJs94rYRK9loJF3xDj8mgQz3LmCyp0yYrNjbN/1/GWZTZDGCA==", + "i18next@25.3.1_typescript@5.8.3": { + "integrity": "sha512-S4CPAx8LfMOnURnnJa8jFWvur+UX/LWcl6+61p9VV7SK2m0445JeBJ6tLD0D5SR0H29G4PYfWkEhivKG5p4RDg==", "dependencies": [ "@babel/runtime", "typescript" @@ -5247,11 +3962,8 @@ "typescript" ] }, - "idb-keyval@6.2.1": { - "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==" - }, - "idb@7.1.1": { - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + "idb-keyval@6.2.2": { + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==" }, "ieee754@1.2.1": { "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" @@ -5259,110 +3971,21 @@ "immer@10.1.1": { "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==" }, - "importx@0.5.2_esbuild@0.25.3": { - "integrity": "sha512-YEwlK86Ml5WiTxN/ECUYC5U7jd1CisAVw7ya4i9ZppBoHfFkT2+hChhr3PE2fYxUKLkNyivxEQpa5Ruil1LJBQ==", - "dependencies": [ - "bundle-require", - "debug", - "esbuild", - "jiti", - "pathe", - "tsx" - ] - }, "indent-string@4.0.0": { "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" }, - "inflight@1.0.6": { - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": [ - "once", - "wrappy" - ], - "deprecated": true - }, "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini@4.1.3": { "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==" }, - "internal-slot@1.1.0": { - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dependencies": [ - "es-errors", - "hasown", - "side-channel" - ] - }, - "is-arguments@1.2.0": { - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dependencies": [ - "call-bound", - "has-tostringtag" - ] - }, - "is-array-buffer@3.0.5": { - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dependencies": [ - "call-bind", - "call-bound", - "get-intrinsic" - ] - }, - "is-async-function@2.1.1": { - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dependencies": [ - "async-function", - "call-bound", - "get-proto", - "has-tostringtag", - "safe-regex-test" - ] - }, - "is-bigint@1.1.0": { - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dependencies": [ - "has-bigints" - ] - }, "is-binary-path@2.1.0": { "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": [ "binary-extensions" ] }, - "is-boolean-object@1.2.2": { - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dependencies": [ - "call-bound", - "has-tostringtag" - ] - }, - "is-callable@1.2.7": { - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" - }, - "is-core-module@2.16.1": { - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dependencies": [ - "hasown" - ] - }, - "is-data-view@1.0.2": { - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dependencies": [ - "call-bound", - "get-intrinsic", - "is-typed-array" - ] - }, - "is-date-object@1.1.0": { - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dependencies": [ - "call-bound", - "has-tostringtag" - ] - }, "is-extendable@0.1.1": { "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" }, @@ -5375,20 +3998,8 @@ "is-extglob@2.1.1": { "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, - "is-finalizationregistry@1.1.1": { - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dependencies": [ - "call-bound" - ] - }, - "is-generator-function@1.1.0": { - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dependencies": [ - "call-bound", - "get-proto", - "has-tostringtag", - "safe-regex-test" - ] + "is-fullwidth-code-point@3.0.0": { + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob@4.0.3": { "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", @@ -5396,107 +4007,23 @@ "is-extglob" ] }, - "is-map@2.0.3": { - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==" - }, - "is-module@1.0.0": { - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "is-nan@1.3.2": { - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dependencies": [ - "call-bind", - "define-properties" - ] - }, - "is-number-object@1.1.1": { - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dependencies": [ - "call-bound", - "has-tostringtag" - ] - }, "is-number@7.0.0": { "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, - "is-obj@1.0.1": { - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==" - }, "is-plain-object@2.0.4": { "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dependencies": [ "isobject" ] }, - "is-regex@1.2.1": { - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dependencies": [ - "call-bound", - "gopd", - "has-tostringtag", - "hasown" - ] - }, - "is-regexp@1.0.0": { - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" - }, - "is-set@2.0.3": { - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==" - }, - "is-shared-array-buffer@1.0.4": { - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dependencies": [ - "call-bound" - ] - }, - "is-stream@2.0.1": { - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - }, - "is-string@1.1.1": { - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dependencies": [ - "call-bound", - "has-tostringtag" - ] - }, - "is-symbol@1.1.1": { - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dependencies": [ - "call-bound", - "has-symbols", - "safe-regex-test" - ] - }, - "is-typed-array@1.1.15": { - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dependencies": [ - "which-typed-array" - ] - }, - "is-weakmap@2.0.2": { - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==" - }, - "is-weakref@1.1.1": { - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dependencies": [ - "call-bound" - ] - }, - "is-weakset@2.0.4": { - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dependencies": [ - "call-bound", - "get-intrinsic" - ] - }, "is-zst@1.0.0": { "integrity": "sha512-ZA5lvshKAl8z30dX7saXLpVhpsq3d2EHK9uf7qtUjnOtdw4XBpAoWb2RvZ5kyoaebdoidnGI0g2hn9Z7ObPbww==" }, "isarray@1.0.0": { "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, - "isarray@2.0.5": { - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "isbot@5.1.28": { + "integrity": "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==" }, "isexe@3.1.1": { "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" @@ -5504,19 +4031,6 @@ "isobject@3.0.1": { "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" }, - "isomorphic-timers-promises@1.0.1": { - "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==" - }, - "jake@10.9.2": { - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dependencies": [ - "async", - "chalk@4.1.2", - "filelist", - "minimatch@3.1.2" - ], - "bin": true - }, "jiti@2.4.2": { "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "bin": true @@ -5527,27 +4041,13 @@ "js-tokens@4.0.0": { "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "js-yaml@4.1.0": { - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": [ - "argparse" - ], - "bin": true - }, - "jsesc@3.0.2": { - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "bin": true + "js-tokens@9.0.1": { + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==" }, "jsesc@3.1.0": { "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": true }, - "json-schema-traverse@1.0.0": { - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "json-schema@0.4.0": { - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, "json-stringify-pretty-compact@3.0.0": { "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==" }, @@ -5558,18 +4058,6 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": true }, - "jsonfile@6.1.0": { - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": [ - "universalify" - ], - "optionalDependencies": [ - "graceful-fs" - ] - }, - "jsonpointer@5.0.1": { - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==" - }, "jsts@2.7.1": { "integrity": "sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg==" }, @@ -5579,70 +4067,58 @@ "kind-of@6.0.3": { "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, - "language-subtag-registry@0.3.23": { - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==" - }, - "language-tags@2.1.0": { - "integrity": "sha512-D4CgpyCt+61f6z2jHjJS1OmZPviAWM57iJ9OKdFFWSNgS7Udj9QVWqyGs/cveVNF57XpZmhSvMdVIV5mjLA7Vg==", - "dependencies": [ - "language-subtag-registry" - ] - }, - "leven@3.1.0": { - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" - }, - "lightningcss-darwin-arm64@1.29.2": { - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "lightningcss-darwin-arm64@1.30.1": { + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "lightningcss-darwin-x64@1.29.2": { - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "lightningcss-darwin-x64@1.30.1": { + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", "os": ["darwin"], "cpu": ["x64"] }, - "lightningcss-freebsd-x64@1.29.2": { - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "lightningcss-freebsd-x64@1.30.1": { + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", "os": ["freebsd"], "cpu": ["x64"] }, - "lightningcss-linux-arm-gnueabihf@1.29.2": { - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "lightningcss-linux-arm-gnueabihf@1.30.1": { + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", "os": ["linux"], "cpu": ["arm"] }, - "lightningcss-linux-arm64-gnu@1.29.2": { - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "lightningcss-linux-arm64-gnu@1.30.1": { + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", "os": ["linux"], "cpu": ["arm64"] }, - "lightningcss-linux-arm64-musl@1.29.2": { - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "lightningcss-linux-arm64-musl@1.30.1": { + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", "os": ["linux"], "cpu": ["arm64"] }, - "lightningcss-linux-x64-gnu@1.29.2": { - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "lightningcss-linux-x64-gnu@1.30.1": { + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "os": ["linux"], "cpu": ["x64"] }, - "lightningcss-linux-x64-musl@1.29.2": { - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "lightningcss-linux-x64-musl@1.30.1": { + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "os": ["linux"], "cpu": ["x64"] }, - "lightningcss-win32-arm64-msvc@1.29.2": { - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "lightningcss-win32-arm64-msvc@1.30.1": { + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", "os": ["win32"], "cpu": ["arm64"] }, - "lightningcss-win32-x64-msvc@1.29.2": { - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "lightningcss-win32-x64-msvc@1.30.1": { + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "os": ["win32"], "cpu": ["x64"] }, - "lightningcss@1.29.2": { - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "lightningcss@1.30.1": { + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "dependencies": [ "detect-libc" ], @@ -5659,36 +4135,15 @@ "lightningcss-win32-x64-msvc" ] }, - "load-tsconfig@0.2.5": { - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==" - }, - "locate-path@6.0.0": { - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": [ - "p-locate@5.0.0" - ] - }, - "locate-path@7.2.0": { - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dependencies": [ - "p-locate@6.0.0" - ] - }, - "lodash.debounce@4.0.8": { - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, "lodash.isequal@4.5.0": { "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "deprecated": true }, - "lodash.sortby@4.7.0": { - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" - }, "lodash@4.17.21": { "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "loupe@3.1.3": { - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==" + "loupe@3.1.4": { + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==" }, "lru-cache@5.1.1": { "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", @@ -5696,8 +4151,8 @@ "yallist@3.1.1" ] }, - "lucide-react@0.507.0_react@19.1.0": { - "integrity": "sha512-XfgE6gvAHwAtnbUvWiTTHx4S3VGR+cUJHEc0vrh9Ogu672I1Tue2+Cp/8JJqpytgcBHAB1FVI297W4XGNwc2dQ==", + "lucide-react@0.525.0_react@19.1.0": { + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", "dependencies": [ "react" ] @@ -5706,20 +4161,14 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "bin": true }, - "magic-string@0.25.9": { - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dependencies": [ - "sourcemap-codec" - ] - }, "magic-string@0.30.17": { "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dependencies": [ "@jridgewell/sourcemap-codec" ] }, - "maplibre-gl@5.4.0": { - "integrity": "sha512-ZVrtdFIhFAqt53H2k5Ssqn7QIKNI19fW+He5tr4loxZxWZffp1aZYY9ImNncAJaALU/NYlV6Eul7UVB56/N7WQ==", + "maplibre-gl@5.6.1": { + "integrity": "sha512-TTSfoTaF7RqKUR9wR5qDxCHH2J1XfZ1E85luiLOx0h8r50T/LnwAwwfV0WVNh9o8dA7rwt57Ucivf1emyeukXg==", "dependencies": [ "@mapbox/geojson-rewind", "@mapbox/jsonlint-lines-primitives", @@ -5728,7 +4177,7 @@ "@mapbox/unitbezier", "@mapbox/vector-tile", "@mapbox/whoots-js", - "@maplibre/maplibre-gl-style-spec@23.2.2", + "@maplibre/maplibre-gl-style-spec@23.3.0", "@types/geojson", "@types/geojson-vt", "@types/mapbox__point-geometry", @@ -5752,56 +4201,9 @@ "marchingsquares@1.3.3": { "integrity": "sha512-gz6nNQoVK7Lkh2pZulrT4qd4347S/toG9RXH2pyzhLgkL5mLkBoqgv4EvAGXcV0ikDW72n/OQb3Xe8bGagQZCg==" }, - "math-intrinsics@1.1.0": { - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "md5.js@1.3.5": { - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dependencies": [ - "hash-base", - "inherits", - "safe-buffer@5.2.1" - ] - }, - "merge2@1.4.1": { - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "micromatch@4.0.8": { - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dependencies": [ - "braces", - "picomatch@2.3.1" - ] - }, - "miller-rabin@4.0.1": { - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dependencies": [ - "bn.js@4.12.2", - "brorand" - ], - "bin": true - }, "min-indent@1.0.1": { "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, - "minimalistic-assert@1.0.1": { - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils@1.0.1": { - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, - "minimatch@3.1.2": { - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": [ - "brace-expansion@1.1.11" - ] - }, - "minimatch@5.1.6": { - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": [ - "brace-expansion@2.0.1" - ] - }, "minimist@1.2.8": { "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, @@ -5828,158 +4230,32 @@ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "bin": true }, - "negotiator@1.0.0": { - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" - }, "node-fetch@2.7.0": { "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": [ - "whatwg-url@5.0.0" + "whatwg-url" ] }, "node-releases@2.0.19": { "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, - "node-stdlib-browser@1.3.1": { - "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", - "dependencies": [ - "assert", - "browser-resolve", - "browserify-zlib", - "buffer", - "console-browserify", - "constants-browserify", - "create-require", - "crypto-browserify", - "domain-browser", - "events", - "https-browserify", - "isomorphic-timers-promises", - "os-browserify", - "path-browserify", - "pkg-dir", - "process", - "punycode@1.4.1", - "querystring-es3", - "readable-stream@3.6.2", - "stream-browserify", - "stream-http", - "string_decoder@1.3.0", - "timers-browserify", - "tty-browserify", - "url", - "util", - "vm-browserify" - ] - }, "normalize-path@3.0.0": { "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "normalize-range@0.1.2": { "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, - "object-inspect@1.13.4": { - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" - }, - "object-is@1.1.6": { - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dependencies": [ - "call-bind", - "define-properties" - ] - }, - "object-keys@1.1.1": { - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign@4.1.7": { - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dependencies": [ - "call-bind", - "call-bound", - "define-properties", - "es-object-atoms", - "has-symbols", - "object-keys" - ] - }, "once@1.4.0": { "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": [ "wrappy" ] }, - "os-browserify@0.3.0": { - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" - }, - "own-keys@1.0.1": { - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dependencies": [ - "get-intrinsic", - "object-keys", - "safe-push-apply" - ] - }, - "p-limit@3.1.0": { - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": [ - "yocto-queue@0.1.0" - ] - }, - "p-limit@4.0.0": { - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dependencies": [ - "yocto-queue@1.2.1" - ] - }, - "p-locate@5.0.0": { - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": [ - "p-limit@3.1.0" - ] - }, - "p-locate@6.0.0": { - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dependencies": [ - "p-limit@4.0.0" - ] - }, - "p-map@7.0.3": { - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==" - }, - "pako@1.0.11": { - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parse-asn1@5.1.7": { - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "dependencies": [ - "asn1.js", - "browserify-aes", - "evp_bytestokey", - "hash-base", - "pbkdf2", - "safe-buffer@5.2.1" - ] - }, - "path-browserify@1.0.1": { - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "path-exists@4.0.0": { - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-exists@5.0.0": { - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==" - }, - "path-is-absolute@1.0.1": { - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-parse@1.0.7": { - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, "pathe@2.0.3": { "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" }, - "pathval@2.0.0": { - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==" + "pathval@2.0.1": { + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==" }, "pbf@3.3.0": { "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", @@ -5989,16 +4265,6 @@ ], "bin": true }, - "pbkdf2@3.1.2": { - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dependencies": [ - "create-hash", - "create-hmac", - "ripemd160", - "safe-buffer@5.2.1", - "sha.js" - ] - }, "peek-stream@1.1.3": { "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", "dependencies": [ @@ -6016,12 +4282,6 @@ "picomatch@4.0.2": { "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==" }, - "pkg-dir@5.0.0": { - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dependencies": [ - "find-up@5.0.0" - ] - }, "point-in-polygon-hao@1.2.4": { "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==", "dependencies": [ @@ -6038,14 +4298,11 @@ "splaytree-ts" ] }, - "possible-typed-array-names@1.1.0": { - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==" - }, "postcss-value-parser@4.2.0": { "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, - "postcss@8.5.3": { - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "postcss@8.5.6": { + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dependencies": [ "nanoid", "picocolors", @@ -6055,16 +4312,10 @@ "potpack@2.0.0": { "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" }, - "prettier@3.5.3": { - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "prettier@3.6.2": { + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "bin": true }, - "pretty-bytes@5.6.0": { - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" - }, - "pretty-bytes@6.1.1": { - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==" - }, "pretty-format@27.5.1": { "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dependencies": [ @@ -6082,43 +4333,11 @@ "duplex-maker" ] }, - "process@0.11.10": { - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, "protocol-buffers-schema@3.6.0": { "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, - "public-encrypt@4.0.3": { - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dependencies": [ - "bn.js@4.12.2", - "browserify-rsa", - "create-hash", - "parse-asn1", - "randombytes", - "safe-buffer@5.2.1" - ] - }, - "punycode@1.4.1": { - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - }, - "punycode@2.3.1": { - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - }, - "qrcode-generator@1.4.4": { - "integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==" - }, - "qs@6.14.0": { - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": [ - "side-channel" - ] - }, - "querystring-es3@0.2.1": { - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==" - }, - "queue-microtask@1.2.3": { - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "qrcode-generator@1.5.2": { + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==" }, "quickselect@1.1.1": { "integrity": "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ==" @@ -6127,20 +4346,7 @@ "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" }, "quickselect@3.0.0": { - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" - }, - "randombytes@2.1.0": { - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": [ - "safe-buffer@5.2.1" - ] - }, - "randomfill@1.0.4": { - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dependencies": [ - "randombytes", - "safe-buffer@5.2.1" - ] + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" }, "rbush@2.0.2": { "integrity": "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==", @@ -6168,14 +4374,14 @@ "react" ] }, - "react-hook-form@7.56.2_react@19.1.0": { - "integrity": "sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==", + "react-hook-form@7.60.0_react@19.1.0": { + "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", "dependencies": [ "react" ] }, - "react-i18next@15.5.1_i18next@25.2.0__typescript@5.8.3_react@19.1.0_typescript@5.8.3": { - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", + "react-i18next@15.6.0_i18next@25.3.1__typescript@5.8.3_react@19.1.0_typescript@5.8.3": { + "integrity": "sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==", "dependencies": [ "@babel/runtime", "html-parse-stringify", @@ -6190,7 +4396,7 @@ "react-is@17.0.2": { "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "react-map-gl@8.0.4_maplibre-gl@5.4.0_react@19.1.0_react-dom@19.1.0__react@19.1.0": { + "react-map-gl@8.0.4_maplibre-gl@5.6.1_react@19.1.0_react-dom@19.1.0__react@19.1.0": { "integrity": "sha512-SHdpvFIvswsZBg6BCPcwY/nbKuCo3sJM1Cj7Sd+gA3gFRFOixD+KtZ2XSuUWq2WySL2emYEXEgrLZoXsV4Ut4Q==", "dependencies": [ "@vis.gl/react-mapbox", @@ -6215,7 +4421,7 @@ "react-refresh@0.17.0": { "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==" }, - "react-remove-scroll-bar@2.3.8_@types+react@19.1.2_react@19.1.0": { + "react-remove-scroll-bar@2.3.8_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "dependencies": [ "@types/react", @@ -6227,8 +4433,8 @@ "@types/react" ] }, - "react-remove-scroll@2.6.3_@types+react@19.1.2_react@19.1.0": { - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "react-remove-scroll@2.7.1_@types+react@19.1.8_react@19.1.0": { + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", "dependencies": [ "@types/react", "react", @@ -6242,7 +4448,7 @@ "@types/react" ] }, - "react-style-singleton@2.2.3_@types+react@19.1.2_react@19.1.0": { + "react-style-singleton@2.2.3_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "dependencies": [ "@types/react", @@ -6262,7 +4468,7 @@ "dependencies": [ "core-util-is", "inherits", - "isarray@1.0.0", + "isarray", "process-nextick-args", "safe-buffer@5.1.2", "string_decoder@1.1.1", @@ -6283,6 +4489,16 @@ "picomatch@2.3.1" ] }, + "recast@0.23.11": { + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dependencies": [ + "ast-types", + "esprima", + "source-map@0.6.1", + "tiny-invariant", + "tslib@2.8.1" + ] + }, "redent@3.0.0": { "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dependencies": [ @@ -6290,62 +4506,8 @@ "strip-indent" ] }, - "reflect.getprototypeof@1.0.10": { - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dependencies": [ - "call-bind", - "define-properties", - "es-abstract", - "es-errors", - "es-object-atoms", - "get-intrinsic", - "get-proto", - "which-builtin-type" - ] - }, - "regenerate-unicode-properties@10.2.0": { - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "dependencies": [ - "regenerate" - ] - }, - "regenerate@1.4.2": { - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "regexp.prototype.flags@1.5.4": { - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dependencies": [ - "call-bind", - "define-properties", - "es-errors", - "get-proto", - "gopd", - "set-function-name" - ] - }, - "regexpu-core@6.2.0": { - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "dependencies": [ - "regenerate", - "regenerate-unicode-properties", - "regjsgen", - "regjsparser", - "unicode-match-property-ecmascript", - "unicode-match-property-value-ecmascript" - ] - }, - "regjsgen@0.8.0": { - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" - }, - "regjsparser@0.12.0": { - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "dependencies": [ - "jsesc@3.0.2" - ], - "bin": true - }, - "require-from-string@2.0.2": { - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + "require-directory@2.1.1": { + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "resolve-pkg-maps@1.0.0": { "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" @@ -6356,45 +4518,19 @@ "protocol-buffers-schema" ] }, - "resolve@1.22.10": { - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dependencies": [ - "is-core-module", - "path-parse", - "supports-preserve-symlinks-flag" - ], - "bin": true - }, - "reusify@1.1.0": { - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" - }, "rfc4648@1.5.4": { "integrity": "sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==" }, - "ripemd160@2.0.2": { - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dependencies": [ - "hash-base", - "inherits" - ] - }, "robust-predicates@2.0.4": { "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==" }, "robust-predicates@3.0.2": { "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, - "rollup@2.79.2": { - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "optionalDependencies": [ - "fsevents" - ], - "bin": true - }, - "rollup@4.40.1": { - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", + "rollup@4.44.2": { + "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", "dependencies": [ - "@types/estree@1.0.7" + "@types/estree" ], "optionalDependencies": [ "@rollup/rollup-android-arm-eabi", @@ -6421,12 +4557,6 @@ ], "bin": true }, - "run-parallel@1.2.0": { - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dependencies": [ - "queue-microtask" - ] - }, "rw@1.3.3": { "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, @@ -6436,37 +4566,12 @@ "tslib@1.14.1" ] }, - "safe-array-concat@1.1.3": { - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dependencies": [ - "call-bind", - "call-bound", - "get-intrinsic", - "has-symbols", - "isarray@2.0.5" - ] - }, "safe-buffer@5.1.2": { "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-buffer@5.2.1": { "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, - "safe-push-apply@1.0.0": { - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dependencies": [ - "es-errors", - "isarray@2.0.5" - ] - }, - "safe-regex-test@1.1.0": { - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dependencies": [ - "call-bound", - "es-errors", - "is-regex" - ] - }, "scheduler@0.26.0": { "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, @@ -6474,12 +4579,6 @@ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": true }, - "serialize-javascript@6.0.2": { - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": [ - "randombytes" - ] - }, "seroval-plugins@1.3.2_seroval@1.3.2": { "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", "dependencies": [ @@ -6489,34 +4588,6 @@ "seroval@1.3.2": { "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==" }, - "set-function-length@1.2.2": { - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": [ - "define-data-property", - "es-errors", - "function-bind", - "get-intrinsic", - "gopd", - "has-property-descriptors" - ] - }, - "set-function-name@2.0.2": { - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dependencies": [ - "define-data-property", - "es-errors", - "functions-have-names", - "has-property-descriptors" - ] - }, - "set-proto@1.0.0": { - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dependencies": [ - "dunder-proto", - "es-errors", - "es-object-atoms" - ] - }, "set-value@2.0.1": { "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dependencies": [ @@ -6526,53 +4597,6 @@ "split-string" ] }, - "setimmediate@1.0.5": { - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "sha.js@2.4.11": { - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dependencies": [ - "inherits", - "safe-buffer@5.2.1" - ], - "bin": true - }, - "side-channel-list@1.0.0": { - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": [ - "es-errors", - "object-inspect" - ] - }, - "side-channel-map@1.0.1": { - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect" - ] - }, - "side-channel-weakmap@1.0.2": { - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect", - "side-channel-map" - ] - }, - "side-channel@1.1.0": { - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": [ - "es-errors", - "object-inspect", - "side-channel-list", - "side-channel-map", - "side-channel-weakmap" - ] - }, "siginfo@2.0.0": { "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" }, @@ -6593,9 +4617,6 @@ "skmeans@0.9.7": { "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==" }, - "smob@1.5.0": { - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==" - }, "solid-js@1.9.7_seroval@1.3.2": { "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", "dependencies": [ @@ -6624,25 +4645,11 @@ "source-map-js@1.2.1": { "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, - "source-map-support@0.5.21": { - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": [ - "buffer-from", - "source-map@0.6.1" - ] - }, "source-map@0.6.1": { "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, - "source-map@0.8.0-beta.0": { - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": [ - "whatwg-url@7.1.0" - ] - }, - "sourcemap-codec@1.4.8": { - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": true + "source-map@0.7.4": { + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" }, "splaytree-ts@1.0.2": { "integrity": "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==" @@ -6668,70 +4675,15 @@ "ste-core" ] }, - "stream-browserify@3.0.0": { - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dependencies": [ - "inherits", - "readable-stream@3.6.2" - ] - }, - "stream-http@3.2.0": { - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dependencies": [ - "builtin-status-codes", - "inherits", - "readable-stream@3.6.2", - "xtend" - ] - }, "stream-shift@1.0.3": { "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, - "string.prototype.matchall@4.0.12": { - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dependencies": [ - "call-bind", - "call-bound", - "define-properties", - "es-abstract", - "es-errors", - "es-object-atoms", - "get-intrinsic", - "gopd", - "has-symbols", - "internal-slot", - "regexp.prototype.flags", - "set-function-name", - "side-channel" - ] - }, - "string.prototype.trim@1.2.10": { - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dependencies": [ - "call-bind", - "call-bound", - "define-data-property", - "define-properties", - "es-abstract", - "es-object-atoms", - "has-property-descriptors" - ] - }, - "string.prototype.trimend@1.0.9": { - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dependencies": [ - "call-bind", - "call-bound", - "define-properties", - "es-object-atoms" - ] - }, - "string.prototype.trimstart@1.0.8": { - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "string-width@4.2.3": { + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": [ - "call-bind", - "define-properties", - "es-object-atoms" + "emoji-regex", + "is-fullwidth-code-point", + "strip-ansi" ] }, "string_decoder@1.1.1": { @@ -6746,23 +4698,24 @@ "safe-buffer@5.2.1" ] }, - "stringify-object@3.3.0": { - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": [ - "get-own-enumerable-property-symbols", - "is-obj", - "is-regexp" + "ansi-regex" ] }, - "strip-comments@2.0.1": { - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==" - }, "strip-indent@3.0.0": { "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dependencies": [ "min-indent" ] }, + "strip-literal@3.0.0": { + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dependencies": [ + "js-tokens@9.0.1" + ] + }, "supercluster@8.0.1": { "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", "dependencies": [ @@ -6775,29 +4728,26 @@ "has-flag" ] }, - "supports-preserve-symlinks-flag@1.0.0": { - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, "sweepline-intersections@1.5.0": { "integrity": "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ==", "dependencies": [ "tinyqueue@2.0.3" ] }, - "tailwind-merge@3.2.0": { - "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==" + "tailwind-merge@3.3.1": { + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==" }, - "tailwindcss-animate@1.0.7_tailwindcss@4.1.5": { + "tailwindcss-animate@1.0.7_tailwindcss@4.1.11": { "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", "dependencies": [ "tailwindcss" ] }, - "tailwindcss@4.1.5": { - "integrity": "sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==" + "tailwindcss@4.1.11": { + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" }, - "tapable@2.2.1": { - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" + "tapable@2.2.2": { + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==" }, "tar@7.4.3": { "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", @@ -6810,28 +4760,6 @@ "yallist@5.0.0" ] }, - "temp-dir@2.0.0": { - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==" - }, - "tempy@0.6.0": { - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", - "dependencies": [ - "is-stream", - "temp-dir", - "type-fest@0.16.0", - "unique-string" - ] - }, - "terser@5.39.0": { - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dependencies": [ - "@jridgewell/source-map", - "acorn", - "commander@2.20.3", - "source-map-support" - ], - "bin": true - }, "testing-library@0.0.2_@angular+common@6.1.10__@angular+core@6.1.10___rxjs@6.6.7___zone.js@0.8.29__rxjs@6.6.7_@angular+core@6.1.10__rxjs@6.6.7__zone.js@0.8.29": { "integrity": "sha512-KCbqCCllbgiCXOgmh9MdsgdJ05pmimXGuggtC78pzpxpq/40A3bS+NJoqwCIqZbNnMr6KIZ2mlMZoZCkWVnaWw==", "dependencies": [ @@ -6853,12 +4781,6 @@ "readable-stream@3.6.2" ] }, - "timers-browserify@2.0.12": { - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dependencies": [ - "setimmediate" - ] - }, "tiny-invariant@1.3.3": { "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, @@ -6871,15 +4793,15 @@ "tinyexec@0.3.2": { "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" }, - "tinyglobby@0.2.13_picomatch@4.0.2": { - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "tinyglobby@0.2.14_picomatch@4.0.2": { + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dependencies": [ "fdir", "picomatch@4.0.2" ] }, - "tinypool@1.0.2": { - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==" + "tinypool@1.1.1": { + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==" }, "tinyqueue@2.0.3": { "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" @@ -6890,8 +4812,8 @@ "tinyrainbow@2.0.0": { "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==" }, - "tinyspy@3.0.2": { - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==" + "tinyspy@4.0.3": { + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==" }, "to-regex-range@5.0.1": { "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", @@ -6916,12 +4838,6 @@ "tr46@0.0.3": { "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "tr46@1.0.1": { - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dependencies": [ - "punycode@2.3.1" - ] - }, "tslib@1.14.1": { "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, @@ -6931,8 +4847,8 @@ "tslog@4.9.3": { "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==" }, - "tsx@4.19.4": { - "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "tsx@4.20.3": { + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dependencies": [ "esbuild", "get-tsconfig" @@ -6942,56 +4858,9 @@ ], "bin": true }, - "tty-browserify@0.0.1": { - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" - }, - "type-fest@0.16.0": { - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==" - }, "type-fest@2.19.0": { "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" }, - "typed-array-buffer@1.0.3": { - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dependencies": [ - "call-bound", - "es-errors", - "is-typed-array" - ] - }, - "typed-array-byte-length@1.0.3": { - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dependencies": [ - "call-bind", - "for-each", - "gopd", - "has-proto", - "is-typed-array" - ] - }, - "typed-array-byte-offset@1.0.4": { - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dependencies": [ - "available-typed-arrays", - "call-bind", - "for-each", - "gopd", - "has-proto", - "is-typed-array", - "reflect.getprototypeof" - ] - }, - "typed-array-length@1.0.7": { - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dependencies": [ - "call-bind", - "for-each", - "gopd", - "is-typed-array", - "possible-typed-array-names", - "reflect.getprototypeof" - ] - }, "typescript@5.8.3": { "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "bin": true @@ -7005,36 +4874,11 @@ "typewise-core" ] }, - "unbox-primitive@1.1.0": { - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dependencies": [ - "call-bound", - "has-bigints", - "has-symbols", - "which-boxed-primitive" - ] - }, "undici-types@6.21.0": { "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, - "unicode-canonical-property-names-ecmascript@2.0.1": { - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==" - }, - "unicode-match-property-ecmascript@2.0.0": { - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dependencies": [ - "unicode-canonical-property-names-ecmascript", - "unicode-property-aliases-ecmascript" - ] - }, - "unicode-match-property-value-ecmascript@2.2.0": { - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==" - }, - "unicode-property-aliases-ecmascript@2.1.0": { - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" - }, - "unicorn-magic@0.1.0": { - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==" + "undici-types@7.8.0": { + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" }, "union-value@1.0.1": { "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", @@ -7045,15 +4889,6 @@ "set-value" ] }, - "unique-string@2.0.0": { - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dependencies": [ - "crypto-random-string@2.0.0" - ] - }, - "universalify@2.0.1": { - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" - }, "unplugin@2.3.5": { "integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", "dependencies": [ @@ -7062,10 +4897,7 @@ "webpack-virtual-modules" ] }, - "upath@1.2.0": { - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" - }, - "update-browserslist-db@1.1.3_browserslist@4.24.5": { + "update-browserslist-db@1.1.3_browserslist@4.25.1": { "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dependencies": [ "browserslist", @@ -7074,14 +4906,7 @@ ], "bin": true }, - "url@0.11.4": { - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", - "dependencies": [ - "punycode@1.4.1", - "qs" - ] - }, - "use-callback-ref@1.3.3_@types+react@19.1.2_react@19.1.0": { + "use-callback-ref@1.3.3_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "dependencies": [ "@types/react", @@ -7092,7 +4917,7 @@ "@types/react" ] }, - "use-sidecar@1.1.3_@types+react@19.1.2_react@19.1.0": { + "use-sidecar@1.1.3_@types+react@19.1.8_react@19.1.0": { "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "dependencies": [ "@types/react", @@ -7113,18 +4938,8 @@ "util-deprecate@1.0.2": { "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "util@0.12.5": { - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": [ - "inherits", - "is-arguments", - "is-generator-function", - "is-typed-array", - "which-typed-array" - ] - }, - "vite-node@3.1.2_@types+node@22.15.3": { - "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", + "vite-node@3.2.4_@types+node@22.16.0": { + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dependencies": [ "cac", "debug", @@ -7134,76 +4949,30 @@ ], "bin": true }, - "vite-plugin-i18n-ally@6.0.1_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@types+node@22.15.3": { - "integrity": "sha512-BmXlAkrmSRrbaho7iJpBf1d2EPyDK2oqY1AKVxkJEUikGDPducFpLfpmlqTyUNhsWZT01ZLWdjR2uIRnnVJXzw==", - "dependencies": [ - "cookie", - "debug", - "deep-object-diff", - "fast-glob", - "find-up@7.0.0", - "importx", - "js-yaml", - "json5", - "language-tags", - "negotiator", - "picocolors", - "vite" - ] - }, - "vite-plugin-node-polyfills@0.23.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@types+node@22.15.3": { - "integrity": "sha512-4n+Ys+2bKHQohPBKigFlndwWQ5fFKwaGY6muNDMTb0fSQLyBzS+jjUNRZG9sKF0S/Go4ApG6LFnUGopjkILg3w==", - "dependencies": [ - "@rollup/plugin-inject", - "node-stdlib-browser", - "vite" - ] - }, - "vite-plugin-pwa@1.0.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_workbox-build@7.3.0__ajv@8.17.1__@babel+core@7.27.1__rollup@2.79.2_workbox-window@7.3.0_@types+node@22.15.3": { - "integrity": "sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==", - "dependencies": [ - "debug", - "pretty-bytes@6.1.1", - "tinyglobby", - "vite", - "workbox-build", - "workbox-window" - ] - }, - "vite-plugin-static-copy@3.0.0_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2_@types+node@22.15.3": { - "integrity": "sha512-Uki9pPUQ4ZnoMEdIFabvoh9h6Bh9Q1m3iF7BrZvoiF30reREpJh2gZb4jOnW1/uYFzyRiLCmFSkM+8hwiq1vWQ==", - "dependencies": [ - "chokidar", - "fs-extra@11.3.0", - "p-map", - "picocolors", - "tinyglobby", - "vite" - ] - }, - "vite@6.3.4_@types+node@22.15.3_picomatch@4.0.2": { - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", + "vite@7.0.2_@types+node@22.16.0_picomatch@4.0.2": { + "integrity": "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==", "dependencies": [ - "@types/node", + "@types/node@22.16.0", "esbuild", "fdir", "picomatch@4.0.2", "postcss", - "rollup@4.40.1", + "rollup", "tinyglobby" ], "optionalDependencies": [ "fsevents" ], "optionalPeers": [ - "@types/node" + "@types/node@22.16.0" ], "bin": true }, - "vitest@3.1.2_@types+node@22.15.3_happy-dom@17.4.6_vite@6.3.4__@types+node@22.15.3__picomatch@4.0.2": { - "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", + "vitest@3.2.4_@types+node@22.16.0_happy-dom@18.0.1_vite@7.0.2__@types+node@22.16.0__picomatch@4.0.2": { + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dependencies": [ - "@types/node", + "@types/chai", + "@types/node@22.16.0", "@vitest/expect", "@vitest/mocker", "@vitest/pretty-format", @@ -7215,8 +4984,9 @@ "debug", "expect-type", "happy-dom", - "magic-string@0.30.17", + "magic-string", "pathe", + "picomatch@4.0.2", "std-env", "tinybench", "tinyexec", @@ -7228,14 +4998,11 @@ "why-is-node-running" ], "optionalPeers": [ - "@types/node", + "@types/node@22.16.0", "happy-dom" ], "bin": true }, - "vm-browserify@1.1.2": { - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" - }, "void-elements@3.1.0": { "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, @@ -7250,12 +5017,6 @@ "webidl-conversions@3.0.1": { "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "webidl-conversions@4.0.2": { - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "webidl-conversions@7.0.0": { - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - }, "webpack-virtual-modules@0.6.2": { "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==" }, @@ -7265,65 +5026,8 @@ "whatwg-url@5.0.0": { "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": [ - "tr46@0.0.3", - "webidl-conversions@3.0.1" - ] - }, - "whatwg-url@7.1.0": { - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dependencies": [ - "lodash.sortby", - "tr46@1.0.1", - "webidl-conversions@4.0.2" - ] - }, - "which-boxed-primitive@1.1.1": { - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dependencies": [ - "is-bigint", - "is-boolean-object", - "is-number-object", - "is-string", - "is-symbol" - ] - }, - "which-builtin-type@1.2.1": { - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dependencies": [ - "call-bound", - "function.prototype.name", - "has-tostringtag", - "is-async-function", - "is-date-object", - "is-finalizationregistry", - "is-generator-function", - "is-regex", - "is-weakref", - "isarray@2.0.5", - "which-boxed-primitive", - "which-collection", - "which-typed-array" - ] - }, - "which-collection@1.0.2": { - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dependencies": [ - "is-map", - "is-set", - "is-weakmap", - "is-weakset" - ] - }, - "which-typed-array@1.1.19": { - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dependencies": [ - "available-typed-arrays", - "call-bind", - "call-bound", - "for-each", - "get-proto", - "gopd", - "has-tostringtag" + "tr46", + "webidl-conversions" ] }, "which@4.0.0": { @@ -7341,144 +5045,12 @@ ], "bin": true }, - "workbox-background-sync@7.3.0": { - "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==", - "dependencies": [ - "idb", - "workbox-core" - ] - }, - "workbox-broadcast-update@7.3.0": { - "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==", - "dependencies": [ - "workbox-core" - ] - }, - "workbox-build@7.3.0_ajv@8.17.1_@babel+core@7.27.1_rollup@2.79.2": { - "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==", - "dependencies": [ - "@apideck/better-ajv-errors", - "@babel/core", - "@babel/preset-env", - "@babel/runtime", - "@rollup/plugin-babel", - "@rollup/plugin-node-resolve", - "@rollup/plugin-replace", - "@rollup/plugin-terser", - "@surma/rollup-plugin-off-main-thread", - "ajv", - "common-tags", - "fast-json-stable-stringify", - "fs-extra@9.1.0", - "glob", - "lodash", - "pretty-bytes@5.6.0", - "rollup@2.79.2", - "source-map@0.8.0-beta.0", - "stringify-object", - "strip-comments", - "tempy", - "upath", - "workbox-background-sync", - "workbox-broadcast-update", - "workbox-cacheable-response", - "workbox-core", - "workbox-expiration", - "workbox-google-analytics", - "workbox-navigation-preload", - "workbox-precaching", - "workbox-range-requests", - "workbox-recipes", - "workbox-routing", - "workbox-strategies", - "workbox-streams", - "workbox-sw", - "workbox-window" - ] - }, - "workbox-cacheable-response@7.3.0": { - "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==", - "dependencies": [ - "workbox-core" - ] - }, - "workbox-core@7.3.0": { - "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==" - }, - "workbox-expiration@7.3.0": { - "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==", - "dependencies": [ - "idb", - "workbox-core" - ] - }, - "workbox-google-analytics@7.3.0": { - "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==", - "dependencies": [ - "workbox-background-sync", - "workbox-core", - "workbox-routing", - "workbox-strategies" - ] - }, - "workbox-navigation-preload@7.3.0": { - "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==", - "dependencies": [ - "workbox-core" - ] - }, - "workbox-precaching@7.3.0": { - "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==", - "dependencies": [ - "workbox-core", - "workbox-routing", - "workbox-strategies" - ] - }, - "workbox-range-requests@7.3.0": { - "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==", - "dependencies": [ - "workbox-core" - ] - }, - "workbox-recipes@7.3.0": { - "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==", - "dependencies": [ - "workbox-cacheable-response", - "workbox-core", - "workbox-expiration", - "workbox-precaching", - "workbox-routing", - "workbox-strategies" - ] - }, - "workbox-routing@7.3.0": { - "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==", + "wrap-ansi@7.0.0": { + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dependencies": [ - "workbox-core" - ] - }, - "workbox-strategies@7.3.0": { - "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==", - "dependencies": [ - "workbox-core" - ] - }, - "workbox-streams@7.3.0": { - "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==", - "dependencies": [ - "workbox-core", - "workbox-routing" - ] - }, - "workbox-sw@7.3.0": { - "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==" - }, - "workbox-window@7.3.0": { - "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==", - "dependencies": [ - "@types/trusted-types", - "workbox-core" + "ansi-styles@4.3.0", + "string-width", + "strip-ansi" ] }, "wrappy@1.0.2": { @@ -7487,26 +5059,38 @@ "xtend@4.0.2": { "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "y18n@5.0.8": { + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, "yallist@3.1.1": { "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yallist@5.0.0": { "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" }, - "yocto-queue@0.1.0": { - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "yargs-parser@21.1.1": { + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, - "yocto-queue@1.2.1": { - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==" + "yargs@17.7.2": { + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": [ + "cliui", + "escalade", + "get-caller-file", + "require-directory", + "string-width", + "y18n", + "yargs-parser" + ] }, - "zod@3.24.3": { - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==" + "zod@3.25.75": { + "integrity": "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==" }, "zone.js@0.8.29": { "integrity": "sha512-mla2acNCMkWXBD+c+yeUrBUrzOxYMNFdQ6FGfigGGtEVBPJx07BQeJekjt9DmH1FtZek4E9rE1eRR9qQpxACOQ==" }, - "zustand@5.0.4_@types+react@19.1.2_immer@10.1.1_react@19.1.0": { - "integrity": "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==", + "zustand@5.0.6_@types+react@19.1.8_immer@10.1.1_react@19.1.0": { + "integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==", "dependencies": [ "@types/react", "immer", @@ -7521,92 +5105,118 @@ }, "workspace": { "dependencies": [ - "jsr:@std/path@^1.1.0" + "jsr:@meshtastic/protobufs@^2.7.0", + "npm:@bufbuild/protobuf@^2.2.3", + "npm:@types/node@^22.13.10", + "npm:ste-simple-events@^3.0.11", + "npm:tslog@^4.9.3" ], - "packageJson": { - "dependencies": [ - "npm:@bufbuild/protobuf@^2.2.5", - "npm:@jsr/meshtastic__core@2.6.2", - "npm:@jsr/meshtastic__transport-http@0.2.1", - "npm:@jsr/meshtastic__transport-web-bluetooth@0.1.2", - "npm:@jsr/meshtastic__transport-web-serial@0.2.1", - "npm:@noble/curves@^1.9.0", - "npm:@radix-ui/react-accordion@^1.2.8", - "npm:@radix-ui/react-checkbox@^1.2.3", - "npm:@radix-ui/react-dialog@^1.1.11", - "npm:@radix-ui/react-dropdown-menu@^2.1.12", - "npm:@radix-ui/react-label@^2.1.4", - "npm:@radix-ui/react-menubar@^1.1.12", - "npm:@radix-ui/react-popover@^1.1.11", - "npm:@radix-ui/react-scroll-area@^1.2.6", - "npm:@radix-ui/react-select@^2.2.2", - "npm:@radix-ui/react-separator@^1.1.4", - "npm:@radix-ui/react-slider@^1.3.2", - "npm:@radix-ui/react-switch@^1.2.2", - "npm:@radix-ui/react-tabs@^1.1.9", - "npm:@radix-ui/react-toast@^1.2.11", - "npm:@radix-ui/react-toggle-group@^1.1.9", - "npm:@radix-ui/react-tooltip@^1.2.4", - "npm:@tailwindcss/postcss@^4.1.5", - "npm:@tanstack/react-router-devtools@^1.120.16", - "npm:@tanstack/react-router@^1.120.15", - "npm:@tanstack/router-devtools@^1.120.15", - "npm:@tanstack/router-plugin@^1.120.15", - "npm:@testing-library/jest-dom@^6.6.3", - "npm:@testing-library/react@^16.3.0", - "npm:@testing-library/user-event@^14.6.1", - "npm:@turf/turf@^7.2.0", - "npm:@types/chrome@^0.0.318", - "npm:@types/js-cookie@^3.0.6", - "npm:@types/node@^22.15.3", - "npm:@types/react-dom@^19.1.3", - "npm:@types/react@^19.1.2", - "npm:@types/serviceworker@^0.0.133", - "npm:@types/w3c-web-serial@^1.0.8", - "npm:@types/web-bluetooth@^0.0.21", - "npm:@vitejs/plugin-react@^4.4.1", - "npm:autoprefixer@^10.4.21", - "npm:base64-js@^1.5.1", - "npm:class-variance-authority@~0.7.1", - "npm:clsx@^2.1.1", - "npm:cmdk@^1.1.1", - "npm:crypto-random-string@5", - "npm:deno@^2.3.5", - "npm:gzipper@^8.2.1", - "npm:happy-dom@^17.4.6", - "npm:i18next-browser-languagedetector@^8.1.0", - "npm:i18next-http-backend@^3.0.2", - "npm:i18next@^25.2.0", - "npm:idb-keyval@^6.2.1", - "npm:immer@^10.1.1", - "npm:js-cookie@^3.0.5", - "npm:lucide-react@0.507", - "npm:maplibre-gl@5.4.0", - "npm:postcss@^8.5.3", - "npm:react-dom@^19.1.0", - "npm:react-error-boundary@6", - "npm:react-hook-form@^7.56.2", - "npm:react-i18next@^15.5.1", - "npm:react-map-gl@8.0.4", - "npm:react-qrcode-logo@3", - "npm:react@^19.1.0", - "npm:rfc4648@^1.5.4", - "npm:simple-git-hooks@^2.13.0", - "npm:tailwind-merge@^3.2.0", - "npm:tailwindcss-animate@^1.0.7", - "npm:tailwindcss@^4.1.5", - "npm:tar@^7.4.3", - "npm:testing-library@^0.0.2", - "npm:typescript@^5.8.3", - "npm:vite-plugin-i18n-ally@^6.0.1", - "npm:vite-plugin-node-polyfills@0.23", - "npm:vite-plugin-pwa@1", - "npm:vite-plugin-static-copy@3", - "npm:vite@^6.3.4", - "npm:vitest@^3.1.2", - "npm:zod@^3.25.0", - "npm:zustand@5.0.4" - ] + "members": { + "packages/core": { + "dependencies": [ + "npm:crc@^4.3.2" + ] + }, + "packages/transport-web-bluetooth": { + "dependencies": [ + "npm:@types/web-bluetooth@^0.0.20" + ] + }, + "packages/transport-web-serial": { + "dependencies": [ + "npm:@types/w3c-web-serial@^1.0.7" + ] + }, + "packages/web": { + "dependencies": [ + "jsr:@std/path@^1.1.0", + "npm:@types/w3c-web-serial@*", + "npm:@types/web-bluetooth@*" + ], + "packageJson": { + "dependencies": [ + "npm:@bufbuild/protobuf@^2.6.0", + "npm:@hookform/resolvers@^5.1.1", + "npm:@jsr/meshtastic__core@2.6.4", + "npm:@jsr/meshtastic__js@2.6.0-0", + "npm:@jsr/meshtastic__transport-http@*", + "npm:@jsr/meshtastic__transport-web-bluetooth@*", + "npm:@jsr/meshtastic__transport-web-serial@*", + "npm:@noble/curves@^1.9.2", + "npm:@radix-ui/react-accordion@^1.2.11", + "npm:@radix-ui/react-checkbox@^1.3.2", + "npm:@radix-ui/react-dialog@^1.1.14", + "npm:@radix-ui/react-dropdown-menu@^2.1.15", + "npm:@radix-ui/react-label@^2.1.7", + "npm:@radix-ui/react-menubar@^1.1.15", + "npm:@radix-ui/react-popover@^1.1.14", + "npm:@radix-ui/react-scroll-area@^1.2.9", + "npm:@radix-ui/react-select@^2.2.5", + "npm:@radix-ui/react-separator@^1.1.7", + "npm:@radix-ui/react-slider@^1.3.5", + "npm:@radix-ui/react-switch@^1.2.5", + "npm:@radix-ui/react-tabs@^1.1.12", + "npm:@radix-ui/react-toast@^1.2.14", + "npm:@radix-ui/react-toggle-group@^1.1.10", + "npm:@radix-ui/react-tooltip@^1.2.7", + "npm:@tailwindcss/postcss@^4.1.11", + "npm:@tanstack/react-router-devtools@^1.125.4", + "npm:@tanstack/react-router@^1.125.4", + "npm:@tanstack/router-cli@^1.125.4", + "npm:@tanstack/router-devtools@^1.125.4", + "npm:@tanstack/router-plugin@^1.125.5", + "npm:@testing-library/jest-dom@^6.6.3", + "npm:@testing-library/react@^16.3.0", + "npm:@testing-library/user-event@^14.6.1", + "npm:@turf/turf@^7.2.0", + "npm:@types/chrome@^0.0.329", + "npm:@types/js-cookie@^3.0.6", + "npm:@types/node@^24.0.10", + "npm:@types/react-dom@^19.1.6", + "npm:@types/react@^19.1.8", + "npm:@types/serviceworker@^0.0.140", + "npm:@types/w3c-web-serial@^1.0.8", + "npm:@types/web-bluetooth@^0.0.21", + "npm:@vitejs/plugin-react@^4.6.0", + "npm:autoprefixer@^10.4.21", + "npm:base64-js@^1.5.1", + "npm:class-variance-authority@~0.7.1", + "npm:clsx@^2.1.1", + "npm:cmdk@^1.1.1", + "npm:crypto-random-string@5", + "npm:gzipper@^8.2.1", + "npm:happy-dom@^18.0.1", + "npm:i18next-browser-languagedetector@^8.2.0", + "npm:i18next-http-backend@^3.0.2", + "npm:i18next@^25.3.1", + "npm:idb-keyval@^6.2.2", + "npm:immer@^10.1.1", + "npm:js-cookie@^3.0.5", + "npm:lucide-react@0.525", + "npm:maplibre-gl@5.6.1", + "npm:postcss@^8.5.6", + "npm:react-dom@^19.1.0", + "npm:react-error-boundary@6", + "npm:react-hook-form@^7.60.0", + "npm:react-i18next@^15.6.0", + "npm:react-map-gl@8.0.4", + "npm:react-qrcode-logo@3", + "npm:react@^19.1.0", + "npm:rfc4648@^1.5.4", + "npm:simple-git-hooks@^2.13.0", + "npm:tailwind-merge@^3.3.1", + "npm:tailwindcss-animate@^1.0.7", + "npm:tailwindcss@^4.1.11", + "npm:tar@^7.4.3", + "npm:testing-library@^0.0.2", + "npm:typescript@^5.8.3", + "npm:vite@7", + "npm:vitest@^3.2.4", + "npm:zod@^3.25.75", + "npm:zustand@5.0.6" + ] + } + } } } } diff --git a/package.json b/package.json deleted file mode 100644 index 26caa6b8..00000000 --- a/package.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "name": "@meshtastic/web", - "version": "2.6.0-0", - "type": "module", - "description": "Meshtastic web client", - "license": "GPL-3.0-only", - "scripts": { - "build": "vite build", - "build:analyze": "BUNDLE_ANALYZE=true deno task build", - "lint": "deno lint src/", - "lint:fix": "deno lint --fix src/", - "format": "deno fmt src/", - "dev": "deno task dev:ui", - "dev:ui": "VITE_APP_VERSION=development deno run -A npm:vite dev", - "test": "deno run -A npm:vitest", - "preview": "deno run -A npm:vite preview", - "package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ." - }, - "repository": { - "type": "git", - "url": "git+https://github.com/meshtastic/web.git" - }, - "bugs": { - "url": "https://github.com/meshtastic/web/issues" - }, - "simple-git-hooks": { - "pre-commit": "deno task lint:fix && deno task format" - }, - "lint-staged": { - "*.{ts,tsx}": [ - "deno task lint:fix", - "deno task format" - ] - }, - "homepage": "https://meshtastic.org", - "dependencies": { - "@bufbuild/protobuf": "^2.2.5", - "@meshtastic/core": "npm:@jsr/meshtastic__core@2.6.2", - "@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http@0.2.1", - "@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth@0.1.2", - "@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial@0.2.1", - "@noble/curves": "^1.9.0", - "@radix-ui/react-accordion": "^1.2.8", - "@radix-ui/react-checkbox": "^1.2.3", - "@radix-ui/react-dialog": "^1.1.11", - "@radix-ui/react-dropdown-menu": "^2.1.12", - "@radix-ui/react-label": "^2.1.4", - "@radix-ui/react-menubar": "^1.1.12", - "@radix-ui/react-popover": "^1.1.11", - "@radix-ui/react-scroll-area": "^1.2.6", - "@radix-ui/react-select": "^2.2.2", - "@radix-ui/react-separator": "^1.1.4", - "@radix-ui/react-slider": "^1.3.2", - "@radix-ui/react-switch": "^1.2.2", - "@radix-ui/react-tabs": "^1.1.9", - "@radix-ui/react-toast": "^1.2.11", - "@radix-ui/react-toggle-group": "^1.1.9", - "@radix-ui/react-tooltip": "^1.2.4", - "@tanstack/react-router": "^1.120.15", - "@tanstack/react-router-devtools": "^1.120.16", - "@tanstack/router-devtools": "^1.120.15", - "@turf/turf": "^7.2.0", - "base64-js": "^1.5.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "crypto-random-string": "^5.0.0", - "deno": "^2.3.5", - "i18next": "^25.2.0", - "i18next-browser-languagedetector": "^8.1.0", - "i18next-http-backend": "^3.0.2", - "idb-keyval": "^6.2.1", - "immer": "^10.1.1", - "js-cookie": "^3.0.5", - "lucide-react": "^0.507.0", - "maplibre-gl": "5.4.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-error-boundary": "^6.0.0", - "react-hook-form": "^7.56.2", - "react-i18next": "^15.5.1", - "react-map-gl": "8.0.4", - "react-qrcode-logo": "^3.0.0", - "rfc4648": "^1.5.4", - "vite-plugin-i18n-ally": "^6.0.1", - "vite-plugin-node-polyfills": "^0.23.0", - "zod": "^3.25.0", - "zustand": "5.0.4" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.5", - "@tanstack/router-plugin": "^1.120.15", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.3.0", - "@testing-library/user-event": "^14.6.1", - "@types/chrome": "^0.0.318", - "@types/js-cookie": "^3.0.6", - "@types/node": "^22.15.3", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", - "@types/serviceworker": "^0.0.133", - "@types/w3c-web-serial": "^1.0.8", - "@types/web-bluetooth": "^0.0.21", - "@vitejs/plugin-react": "^4.4.1", - "autoprefixer": "^10.4.21", - "gzipper": "^8.2.1", - "happy-dom": "^17.4.6", - "postcss": "^8.5.3", - "simple-git-hooks": "^2.13.0", - "tailwind-merge": "^3.2.0", - "tailwindcss": "^4.1.5", - "tailwindcss-animate": "^1.0.7", - "tar": "^7.4.3", - "testing-library": "^0.0.2", - "typescript": "^5.8.3", - "vite": "^6.3.4", - "vite-plugin-pwa": "^1.0.0", - "vite-plugin-static-copy": "^3.0.0", - "vitest": "^3.1.2" - } -} diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..18ce7cf7 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,26 @@ +# @meshtastic/core + +[![JSR](https://jsr.io/badges/@meshtastic/core)](https://jsr.io/@meshtastic/core) +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml) +[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js) +[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) +[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) + +## Overview + +`@meshtastic/core` Provides core functionality for interfacing with Meshtastic +devices. Installation instructions are available at +[JSR](https://jsr.io/@meshtastic/core) + +## Usage + +```ts +import { MeshDevice } from "@meshtastic/core"; + +// Transport if provided by one of the available transport adapters +const device = new MeshDevice(transport); +``` + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") diff --git a/packages/core/deno.json b/packages/core/deno.json new file mode 100644 index 00000000..fb29ac95 --- /dev/null +++ b/packages/core/deno.json @@ -0,0 +1,10 @@ +{ + "name": "@meshtastic/core", + "version": "2.6.4", + "exports": { + ".": "./mod.ts" + }, + "imports": { + "crc": "npm:crc@^4.3.2" + } +} diff --git a/packages/core/mod.ts b/packages/core/mod.ts new file mode 100644 index 00000000..dd991929 --- /dev/null +++ b/packages/core/mod.ts @@ -0,0 +1,5 @@ +export { Constants } from "./src/constants.ts"; +export { MeshDevice } from "./src/meshDevice.ts"; +export * as Protobuf from "@meshtastic/protobufs"; +export * as Types from "./src/types.ts"; +export * as Utils from "./src/utils/mod.ts"; diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts new file mode 100644 index 00000000..01053c3f --- /dev/null +++ b/packages/core/src/constants.ts @@ -0,0 +1,10 @@ +/** Broadcast destination number */ +const broadcastNum = 0xffffffff; + +/** Minimum device firmware version supported by this version of the library. */ +const minFwVer = 2.2; + +export const Constants = { + broadcastNum, + minFwVer, +}; diff --git a/packages/core/src/meshDevice.ts b/packages/core/src/meshDevice.ts new file mode 100755 index 00000000..36a2dcfa --- /dev/null +++ b/packages/core/src/meshDevice.ts @@ -0,0 +1,1177 @@ +import { Logger } from "tslog"; + +import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; +import * as Protobuf from "@meshtastic/protobufs"; + +import { EventSystem, Queue, Xmodem } from "./utils/mod.ts"; +import { decodePacket } from "./utils/transform/decodePacket.ts"; +import { Constants } from "./constants.ts"; +import type { Destination, PacketMetadata, Transport } from "./types.ts"; +import { ChannelNumber, DeviceStatusEnum, Emitter } from "./types.ts"; + +export class MeshDevice { + public transport: Transport; + + /** Logs to the console and the logging event emitter */ + public log: Logger; + + /** Describes the current state of the device */ + protected deviceStatus: DeviceStatusEnum; + + /** Describes the current state of the device */ + protected isConfigured: boolean; + + /** Are there any settings that have yet to be applied? */ + protected pendingSettingsChanges: boolean; + + /** Device's node number */ + private myNodeInfo: Protobuf.Mesh.MyNodeInfo; + + /** Randomly generated number to ensure confiuration lockstep */ + public configId: number; + + /** + * Packert queue, to space out transmissions and routing handle errors and + * acks + */ + public queue: Queue; + + public events: EventSystem; + + public xModem: Xmodem; + + constructor(transport: Transport, configId?: number) { + this.log = new Logger({ + name: "iMeshDevice", + prettyLogTemplate: + "{{hh}}:{{MM}}:{{ss}}:{{ms}}\t{{logLevelName}}\t[{{name}}]\t", + }); + + this.transport = transport; + this.deviceStatus = DeviceStatusEnum.DeviceDisconnected; + this.isConfigured = false; + this.pendingSettingsChanges = false; + this.myNodeInfo = create(Protobuf.Mesh.MyNodeInfoSchema); + this.configId = configId ?? this.generateRandId(); + this.queue = new Queue(); + this.events = new EventSystem(); + this.xModem = new Xmodem(this.sendRaw.bind(this)); //TODO: try wihtout bind + + this.events.onDeviceStatus.subscribe((status) => { + this.deviceStatus = status; + if (status === DeviceStatusEnum.DeviceConfigured) { + this.isConfigured = true; + } else if (status === DeviceStatusEnum.DeviceConfiguring) { + this.isConfigured = false; + } + }); + + this.events.onMyNodeInfo.subscribe((myNodeInfo) => { + this.myNodeInfo = myNodeInfo; + }); + + this.events.onPendingSettingsChange.subscribe((state) => { + this.pendingSettingsChanges = state; + }); + + this.transport.fromDevice.pipeTo(decodePacket(this)); + } + + /** Abstract method that connects to the radio */ + // protected abstract connect( + // parameters: Types.ConnectionParameters, + // ): Promise; + + /** Abstract method that disconnects from the radio */ + // protected abstract disconnect(): void; + + /** Abstract method that pings the radio */ + // protected abstract ping(): Promise; + + /** + * Sends a text over the radio + */ + public async sendText( + text: string, + destination?: Destination, + wantAck?: boolean, + channel?: ChannelNumber, + replyId?: number, + emoji?: number, + ): Promise { + this.log.debug( + Emitter[Emitter.SendText], + `📤 Sending message to ${destination ?? "broadcast"} on channel ${ + channel?.toString() ?? 0 + }`, + ); + + const enc = new TextEncoder(); + + return await this.sendPacket( + enc.encode(text), + Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP, + destination ?? "broadcast", + channel, + wantAck, + false, + true, + replyId, + emoji, + ); + } + + /** + * Sends a text over the radio + */ + public sendWaypoint( + waypointMessage: Protobuf.Mesh.Waypoint, + destination: Destination, + channel?: ChannelNumber, + ): Promise { + this.log.debug( + Emitter[Emitter.SendWaypoint], + `📤 Sending waypoint to ${destination} on channel ${ + channel?.toString() ?? 0 + }`, + ); + + waypointMessage.id = this.generateRandId(); + + return this.sendPacket( + toBinary(Protobuf.Mesh.WaypointSchema, waypointMessage), + Protobuf.Portnums.PortNum.WAYPOINT_APP, + destination, + channel, + true, + false, + ); + } + + /** + * Sends packet over the radio + */ + public async sendPacket( + byteData: Uint8Array, + portNum: Protobuf.Portnums.PortNum, + destination: Destination, + channel: ChannelNumber = ChannelNumber.Primary, + wantAck = true, + wantResponse = true, + echoResponse = false, + replyId?: number, + emoji?: number, + ): Promise { + this.log.trace( + Emitter[Emitter.SendPacket], + `📤 Sending ${Protobuf.Portnums.PortNum[portNum]} to ${destination}`, + ); + + const meshPacket = create(Protobuf.Mesh.MeshPacketSchema, { + payloadVariant: { + case: "decoded", + value: { + payload: byteData, + portnum: portNum, + wantResponse, + emoji, + replyId, + dest: 0, //change this! + requestId: 0, //change this! + source: 0, //change this! + }, + }, + from: this.myNodeInfo.myNodeNum, + to: destination === "broadcast" + ? Constants.broadcastNum + : destination === "self" + ? this.myNodeInfo.myNodeNum + : destination, + id: this.generateRandId(), + wantAck: wantAck, + channel, + }); + + const toRadioMessage = create(Protobuf.Mesh.ToRadioSchema, { + payloadVariant: { + case: "packet", + value: meshPacket, + }, + }); + + if (echoResponse) { + meshPacket.rxTime = Math.trunc(new Date().getTime() / 1000); + this.handleMeshPacket(meshPacket); + } + return await this.sendRaw( + toBinary(Protobuf.Mesh.ToRadioSchema, toRadioMessage), + meshPacket.id, + ); + } + + /** + * Sends raw packet over the radio + */ + public async sendRaw( + toRadio: Uint8Array, + id: number = this.generateRandId(), + ): Promise { + if (toRadio.length > 512) { + throw new Error("Message longer than 512 bytes, it will not be sent!"); + } + this.queue.push({ + id, + data: toRadio, + }); + + await this.queue.processQueue(this.transport.toDevice); + + return this.queue.wait(id); + } + + /** + * Writes config to device + */ + public async setConfig(config: Protobuf.Config.Config): Promise { + this.log.debug( + Emitter[Emitter.SetConfig], + `⚙️ Setting config, Variant: ${config.payloadVariant.case ?? "Unknown"}`, + ); + + if (!this.pendingSettingsChanges) { + await this.beginEditSettings(); + } + + const configMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "setConfig", + value: config, + }, + }); + + return this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, configMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Writes module config to device + */ + public async setModuleConfig( + moduleConfig: Protobuf.ModuleConfig.ModuleConfig, + ): Promise { + this.log.debug( + Emitter[Emitter.SetModuleConfig], + "⚙️ Setting module config", + ); + + const moduleConfigMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "setModuleConfig", + value: moduleConfig, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, moduleConfigMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + // Write cannedMessages to device + public async setCannedMessages( + cannedMessages: Protobuf.CannedMessages.CannedMessageModuleConfig, + ): Promise { + this.log.debug( + Emitter[Emitter.SetCannedMessages], + "⚙️ Setting CannedMessages", + ); + + const cannedMessagesMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "setCannedMessageModuleMessages", + value: cannedMessages.messages, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, cannedMessagesMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Sets devices owner data + */ + public async setOwner(owner: Protobuf.Mesh.User): Promise { + this.log.debug(Emitter[Emitter.SetOwner], "👤 Setting owner"); + + const setOwnerMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "setOwner", + value: owner, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, setOwnerMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Sets devices ChannelSettings + */ + public async setChannel(channel: Protobuf.Channel.Channel): Promise { + this.log.debug( + Emitter[Emitter.SetChannel], + `📻 Setting Channel: ${channel.index}`, + ); + + const setChannelMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "setChannel", + value: channel, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, setChannelMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Triggers Device to enter DFU mode + */ + public async enterDfuMode(): Promise { + this.log.debug( + Emitter[Emitter.EnterDfuMode], + "🔌 Entering DFU mode", + ); + + const enterDfuModeRequest = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "enterDfuModeRequest", + value: true, + }, + }); + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, enterDfuModeRequest), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Sets static position of device + */ + public async setPosition( + positionMessage: Protobuf.Mesh.Position, + ): Promise { + return await this.sendPacket( + toBinary(Protobuf.Mesh.PositionSchema, positionMessage), + Protobuf.Portnums.PortNum.POSITION_APP, + "self", + ); + } + + /** + * Gets specified channel information from the radio + */ + public async getChannel(index: number): Promise { + this.log.debug( + Emitter[Emitter.GetChannel], + `📻 Requesting Channel: ${index}`, + ); + + const getChannelRequestMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "getChannelRequest", + value: index + 1, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, getChannelRequestMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Gets devices config + */ + public async getConfig( + configType: Protobuf.Admin.AdminMessage_ConfigType, + ): Promise { + this.log.debug( + Emitter[Emitter.GetConfig], + "⚙️ Requesting config", + ); + + const getRadioRequestMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "getConfigRequest", + value: configType, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, getRadioRequestMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Gets Module config + */ + public async getModuleConfig( + moduleConfigType: Protobuf.Admin.AdminMessage_ModuleConfigType, + ): Promise { + this.log.debug( + Emitter[Emitter.GetModuleConfig], + "⚙️ Requesting module config", + ); + + const getRadioRequestMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "getModuleConfigRequest", + value: moduleConfigType, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, getRadioRequestMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** Gets devices Owner */ + public async getOwner(): Promise { + this.log.debug( + Emitter[Emitter.GetOwner], + "👤 Requesting owner", + ); + + const getOwnerRequestMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "getOwnerRequest", + value: true, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, getOwnerRequestMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Gets devices metadata + */ + public async getMetadata(nodeNum: number): Promise { + this.log.debug( + Emitter[Emitter.GetMetadata], + `🏷️ Requesting metadata from ${nodeNum}`, + ); + + const getDeviceMetricsRequestMessage = create( + Protobuf.Admin.AdminMessageSchema, + { + payloadVariant: { + case: "getDeviceMetadataRequest", + value: true, + }, + }, + ); + + return await this.sendPacket( + toBinary( + Protobuf.Admin.AdminMessageSchema, + getDeviceMetricsRequestMessage, + ), + Protobuf.Portnums.PortNum.ADMIN_APP, + nodeNum, + ChannelNumber.Admin, + ); + } + + /** + * Clears specific channel with the designated index + */ + public async clearChannel(index: number): Promise { + this.log.debug( + Emitter[Emitter.ClearChannel], + `📻 Clearing Channel ${index}`, + ); + + const channel = create(Protobuf.Channel.ChannelSchema, { + index, + role: Protobuf.Channel.Channel_Role.DISABLED, + }); + const setChannelMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "setChannel", + value: channel, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, setChannelMessage), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + private async beginEditSettings(): Promise { + this.events.onPendingSettingsChange.dispatch(true); + + const beginEditSettings = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "beginEditSettings", + value: true, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, beginEditSettings), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + public async commitEditSettings(): Promise { + this.events.onPendingSettingsChange.dispatch(false); + + const commitEditSettings = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "commitEditSettings", + value: true, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, commitEditSettings), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Resets the internal NodeDB of the radio, usefull for removing old nodes + * that no longer exist. + */ + public async resetNodes(): Promise { + this.log.debug( + Emitter[Emitter.ResetNodes], + "📻 Resetting NodeDB", + ); + + const resetNodes = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "nodedbReset", + value: 1, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, resetNodes), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Removes a node from the internal NodeDB of the radio by node number + */ + public async removeNodeByNum(nodeNum: number): Promise { + this.log.debug( + Emitter[Emitter.RemoveNodeByNum], + `📻 Removing Node ${nodeNum} from NodeDB`, + ); + + const removeNodeByNum = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "removeByNodenum", + value: nodeNum, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, removeNodeByNum), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** Shuts down the current node after the specified amount of time has elapsed. */ + public async shutdown(time: number): Promise { + this.log.debug( + Emitter[Emitter.Shutdown], + `🔌 Shutting down ${time > 2 ? "now" : `in ${time} seconds`}`, + ); + + const shutdown = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "shutdownSeconds", + value: time, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, shutdown), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** Reboots the current node after the specified amount of time has elapsed. */ + public async reboot(time: number): Promise { + this.log.debug( + Emitter[Emitter.Reboot], + `🔌 Rebooting node ${time > 0 ? "now" : `in ${time} seconds`}`, + ); + + const reboot = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "rebootSeconds", + value: time, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, reboot), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Reboots the current node into OTA mode after the specified amount of time has elapsed. + */ + public async rebootOta(time: number): Promise { + this.log.debug( + Emitter[Emitter.RebootOta], + `🔌 Rebooting into OTA mode ${time > 0 ? "now" : `in ${time} seconds`}`, + ); + + const rebootOta = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "rebootOtaSeconds", + value: time, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, rebootOta), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Factory resets the current device + */ + public async factoryResetDevice(): Promise { + this.log.debug( + Emitter[Emitter.FactoryReset], + "♻️ Factory resetting device", + ); + + const factoryReset = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "factoryResetDevice", + value: 1, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, factoryReset), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Factory resets the current config + */ + public async factoryResetConfig(): Promise { + this.log.debug( + Emitter[Emitter.FactoryReset], + "♻️ Factory resetting config", + ); + + const factoryReset = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant: { + case: "factoryResetConfig", + value: 1, + }, + }); + + return await this.sendPacket( + toBinary(Protobuf.Admin.AdminMessageSchema, factoryReset), + Protobuf.Portnums.PortNum.ADMIN_APP, + "self", + ); + } + + /** + * Triggers the device configure process + */ + public configure(): Promise { + this.log.debug( + Emitter[Emitter.Configure], + "⚙️ Requesting device configuration", + ); + this.updateDeviceStatus(DeviceStatusEnum.DeviceConfiguring); + + const toRadio = create(Protobuf.Mesh.ToRadioSchema, { + payloadVariant: { + case: "wantConfigId", + value: this.configId, + }, + }); + + return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)); + } + + /** + * Serial connection requires a heartbeat ping to stay connected, otherwise times out after 15 minutes + */ + public heartbeat(): Promise { + this.log.debug( + Emitter[Emitter.Ping], + "❤️ Send heartbeat ping to radio", + ); + + const toRadio = create(Protobuf.Mesh.ToRadioSchema, { + payloadVariant: { + case: "heartbeat", + value: {}, + }, + }); + + return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)); + } + + /** + * Sends a trace route packet to the designated node + */ + public async traceRoute(destination: number): Promise { + const routeDiscovery = create(Protobuf.Mesh.RouteDiscoverySchema, { + route: [], + }); + + return await this.sendPacket( + toBinary(Protobuf.Mesh.RouteDiscoverySchema, routeDiscovery), + Protobuf.Portnums.PortNum.TRACEROUTE_APP, + destination, + ); + } + + /** + * Requests position from the designated node + */ + public async requestPosition(destination: number): Promise { + return await this.sendPacket( + new Uint8Array(), + Protobuf.Portnums.PortNum.POSITION_APP, + destination, + ); + } + + /** + * Updates the device status eliminating duplicate status events + */ + public updateDeviceStatus(status: DeviceStatusEnum): void { + if (status !== this.deviceStatus) { + this.events.onDeviceStatus.dispatch(status); + } + } + + /** + * Generates random packet identifier + * + * @returns {number} Random packet ID + */ + private generateRandId(): number { + const seed = crypto.getRandomValues(new Uint32Array(1)); + if (!seed[0]) { + throw new Error("Cannot generate CSPRN"); + } + + return Math.floor(seed[0] * 2 ** -32 * 1e9); + } + + /** Completes all Events */ + public complete(): void { + this.queue.clear(); + } + + /** + * Gets called when a MeshPacket is received from device + */ + public handleMeshPacket(meshPacket: Protobuf.Mesh.MeshPacket): void { + this.events.onMeshPacket.dispatch(meshPacket); + if (meshPacket.from !== this.myNodeInfo.myNodeNum) { + /** + * TODO: this shouldn't be called unless the device interracts with the + * mesh, currently it does. + */ + this.events.onMeshHeartbeat.dispatch(new Date()); + } + + switch (meshPacket.payloadVariant.case) { + case "decoded": { + this.handleDecodedPacket(meshPacket.payloadVariant.value, meshPacket); + break; + } + + case "encrypted": { + this.log.debug( + Emitter[Emitter.HandleMeshPacket], + "🔐 Device received encrypted data packet, ignoring.", + ); + break; + } + + default: + throw new Error(`Unhandled case ${meshPacket.payloadVariant.case}`); + } + } + + private handleDecodedPacket( + dataPacket: Protobuf.Mesh.Data, + meshPacket: Protobuf.Mesh.MeshPacket, + ) { + let adminMessage: Protobuf.Admin.AdminMessage | undefined = undefined; + let routingPacket: Protobuf.Mesh.Routing | undefined = undefined; + + const packetMetadata: Omit, "data"> = { + id: meshPacket.id, + rxTime: new Date(meshPacket.rxTime * 1000), + type: meshPacket.to === Constants.broadcastNum ? "broadcast" : "direct", + from: meshPacket.from, + to: meshPacket.to, + channel: meshPacket.channel, + }; + + this.log.trace( + Emitter[Emitter.HandleMeshPacket], + `📦 Received ${Protobuf.Portnums.PortNum[dataPacket.portnum]} packet`, + ); + + switch (dataPacket.portnum) { + case Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP: { + this.events.onMessagePacket.dispatch({ + ...packetMetadata, + data: new TextDecoder().decode(dataPacket.payload), + }); + break; + } + + case Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP: { + this.events.onRemoteHardwarePacket.dispatch({ + ...packetMetadata, + data: fromBinary( + Protobuf.RemoteHardware.HardwareMessageSchema, + dataPacket.payload, + ), + }); + break; + } + + case Protobuf.Portnums.PortNum.POSITION_APP: { + this.events.onPositionPacket.dispatch({ + ...packetMetadata, + data: fromBinary(Protobuf.Mesh.PositionSchema, dataPacket.payload), + }); + break; + } + + case Protobuf.Portnums.PortNum.NODEINFO_APP: { + this.events.onUserPacket.dispatch({ + ...packetMetadata, + data: fromBinary(Protobuf.Mesh.UserSchema, dataPacket.payload), + }); + break; + } + + case Protobuf.Portnums.PortNum.ROUTING_APP: { + routingPacket = fromBinary( + Protobuf.Mesh.RoutingSchema, + dataPacket.payload, + ); + + this.events.onRoutingPacket.dispatch({ + ...packetMetadata, + data: routingPacket, + }); + switch (routingPacket.variant.case) { + case "errorReason": { + if ( + routingPacket.variant.value === Protobuf.Mesh.Routing_Error.NONE + ) { + this.queue.processAck(dataPacket.requestId); + } else { + this.queue.processError({ + id: dataPacket.requestId, + error: routingPacket.variant.value, + }); + } + + break; + } + case "routeReply": { + break; + } + case "routeRequest": { + break; + } + + default: { + throw new Error(`Unhandled case ${routingPacket.variant.case}`); + } + } + break; + } + + case Protobuf.Portnums.PortNum.ADMIN_APP: { + adminMessage = fromBinary( + Protobuf.Admin.AdminMessageSchema, + dataPacket.payload, + ); + switch (adminMessage.payloadVariant.case) { + case "getChannelResponse": { + this.events.onChannelPacket.dispatch( + adminMessage.payloadVariant.value, + ); + break; + } + case "getOwnerResponse": { + this.events.onUserPacket.dispatch({ + ...packetMetadata, + data: adminMessage.payloadVariant.value, + }); + break; + } + case "getConfigResponse": { + this.events.onConfigPacket.dispatch( + adminMessage.payloadVariant.value, + ); + break; + } + case "getModuleConfigResponse": { + this.events.onModuleConfigPacket.dispatch( + adminMessage.payloadVariant.value, + ); + break; + } + case "getDeviceMetadataResponse": { + this.log.debug( + Emitter[Emitter.GetMetadata], + `🏷️ Received metadata packet from ${dataPacket.source}`, + ); + + this.events.onDeviceMetadataPacket.dispatch({ + ...packetMetadata, + data: adminMessage.payloadVariant.value, + }); + break; + } + case "getCannedMessageModuleMessagesResponse": { + this.log.debug( + Emitter[Emitter.GetMetadata], + `🥫 Received CannedMessage Module Messages response packet`, + ); + + this.events.onCannedMessageModulePacket.dispatch({ + ...packetMetadata, + data: adminMessage.payloadVariant.value, + }); + break; + } + default: { + this.log.error( + Emitter[Emitter.HandleMeshPacket], + `⚠️ Received unhandled AdminMessage, type ${ + adminMessage.payloadVariant.case ?? "undefined" + }`, + dataPacket.payload, + ); + } + } + break; + } + + case Protobuf.Portnums.PortNum.WAYPOINT_APP: { + this.events.onWaypointPacket.dispatch({ + ...packetMetadata, + data: fromBinary(Protobuf.Mesh.WaypointSchema, dataPacket.payload), + }); + break; + } + + case Protobuf.Portnums.PortNum.AUDIO_APP: { + this.events.onAudioPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.DETECTION_SENSOR_APP: { + this.events.onDetectionSensorPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.REPLY_APP: { + this.events.onPingPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, //TODO: decode + }); + break; + } + + case Protobuf.Portnums.PortNum.IP_TUNNEL_APP: { + this.events.onIpTunnelPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.PAXCOUNTER_APP: { + this.events.onPaxcounterPacket.dispatch({ + ...packetMetadata, + data: fromBinary( + Protobuf.PaxCount.PaxcountSchema, + dataPacket.payload, + ), + }); + break; + } + + case Protobuf.Portnums.PortNum.SERIAL_APP: { + this.events.onSerialPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.STORE_FORWARD_APP: { + this.events.onStoreForwardPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.RANGE_TEST_APP: { + this.events.onRangeTestPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.TELEMETRY_APP: { + this.events.onTelemetryPacket.dispatch({ + ...packetMetadata, + data: fromBinary( + Protobuf.Telemetry.TelemetrySchema, + dataPacket.payload, + ), + }); + break; + } + + case Protobuf.Portnums.PortNum.ZPS_APP: { + this.events.onZpsPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.SIMULATOR_APP: { + this.events.onSimulatorPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.TRACEROUTE_APP: { + this.events.onTraceRoutePacket.dispatch({ + ...packetMetadata, + data: fromBinary( + Protobuf.Mesh.RouteDiscoverySchema, + dataPacket.payload, + ), + }); + break; + } + + case Protobuf.Portnums.PortNum.NEIGHBORINFO_APP: { + this.events.onNeighborInfoPacket.dispatch({ + ...packetMetadata, + data: fromBinary( + Protobuf.Mesh.NeighborInfoSchema, + dataPacket.payload, + ), + }); + break; + } + + case Protobuf.Portnums.PortNum.ATAK_PLUGIN: { + this.events.onAtakPluginPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.MAP_REPORT_APP: { + this.events.onMapReportPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.PRIVATE_APP: { + this.events.onPrivatePacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + case Protobuf.Portnums.PortNum.ATAK_FORWARDER: { + this.events.onAtakForwarderPacket.dispatch({ + ...packetMetadata, + data: dataPacket.payload, + }); + break; + } + + default: + throw new Error(`Unhandled case ${dataPacket.portnum}`); + } + } +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 00000000..31457e96 --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,128 @@ +import type * as Protobuf from "@meshtastic/protobufs"; + +interface Packet { + type: "packet"; + data: Uint8Array; +} + +interface DebugLog { + type: "debug"; + data: string; +} + +export type DeviceOutput = Packet | DebugLog; + +export interface Transport { + toDevice: WritableStream; + fromDevice: ReadableStream; +} + +export interface QueueItem { + id: number; + data: Uint8Array; + sent: boolean; + added: Date; + promise: Promise; +} + +export interface HttpRetryConfig { + maxRetries: number; + initialDelayMs: number; + maxDelayMs: number; + backoffFactor: number; +} + +export enum DeviceStatusEnum { + DeviceRestarting = 1, + DeviceDisconnected = 2, + DeviceConnecting = 3, + DeviceReconnecting = 4, + DeviceConnected = 5, + DeviceConfiguring = 6, + DeviceConfigured = 7, +} + +export type LogEventPacket = LogEvent & { date: Date }; + +export type PacketDestination = "broadcast" | "direct"; + +export interface PacketMetadata { + id: number; + rxTime: Date; + type: PacketDestination; + from: number; + to: number; + channel: ChannelNumber; + data: T; +} + +export enum EmitterScope { + MeshDevice = 1, + SerialConnection = 2, + NodeSerialConnection = 3, + BleConnection = 4, + HttpConnection = 5, +} + +export enum Emitter { + Constructor = 0, + SendText = 1, + SendWaypoint = 2, + SendPacket = 3, + SendRaw = 4, + SetConfig = 5, + SetModuleConfig = 6, + ConfirmSetConfig = 7, + SetOwner = 8, + SetChannel = 9, + ConfirmSetChannel = 10, + ClearChannel = 11, + GetChannel = 12, + GetAllChannels = 13, + GetConfig = 14, + GetModuleConfig = 15, + GetOwner = 16, + Configure = 17, + HandleFromRadio = 18, + HandleMeshPacket = 19, + Connect = 20, + Ping = 21, + ReadFromRadio = 22, + WriteToRadio = 23, + SetDebugMode = 24, + GetMetadata = 25, + ResetNodes = 26, + Shutdown = 27, + Reboot = 28, + RebootOta = 29, + FactoryReset = 30, + EnterDfuMode = 31, + RemoveNodeByNum = 32, + SetCannedMessages = 33, +} + +export interface LogEvent { + scope: EmitterScope; + emitter: Emitter; + message: string; + level: Protobuf.Mesh.LogRecord_Level; + packet?: Uint8Array; +} + +export enum ChannelNumber { + Primary = 0, + Channel1 = 1, + Channel2 = 2, + Channel3 = 3, + Channel4 = 4, + Channel5 = 5, + Channel6 = 6, + Admin = 7, +} + +export type Destination = number | "self" | "broadcast"; + +export interface PacketError { + id: number; + error: Protobuf.Mesh.Routing_Error; +} diff --git a/packages/core/src/utils/eventSystem.ts b/packages/core/src/utils/eventSystem.ts new file mode 100644 index 00000000..a2da58c8 --- /dev/null +++ b/packages/core/src/utils/eventSystem.ts @@ -0,0 +1,467 @@ +import { SimpleEventDispatcher } from "ste-simple-events"; +import type * as Protobuf from "@meshtastic/protobufs"; +import type { PacketMetadata } from "../types.ts"; +import type * as Types from "../types.ts"; + +export class EventSystem { + /** + * Fires when a new FromRadio message has been received from the device + * + * @event onLogEvent + */ + public readonly onLogEvent: SimpleEventDispatcher< + Types.LogEventPacket + > = new SimpleEventDispatcher< + Types.LogEventPacket + >(); + + /** + * Fires when a new FromRadio message has been received from the device + * + * @event onFromRadio + */ + public readonly onFromRadio: SimpleEventDispatcher< + Protobuf.Mesh.FromRadio + > = new SimpleEventDispatcher< + Protobuf.Mesh.FromRadio + >(); + + /** + * Fires when a new FromRadio message containing a Data packet has been + * received from the device + * + * @event onMeshPacket + */ + public readonly onMeshPacket: SimpleEventDispatcher< + Protobuf.Mesh.MeshPacket + > = new SimpleEventDispatcher< + Protobuf.Mesh.MeshPacket + >(); + + /** + * Fires when a new MyNodeInfo message has been received from the device + * + * @event onMyNodeInfo + */ + public readonly onMyNodeInfo: SimpleEventDispatcher< + Protobuf.Mesh.MyNodeInfo + > = new SimpleEventDispatcher< + Protobuf.Mesh.MyNodeInfo + >(); + + /** + * Fires when a new MeshPacket message containing a NodeInfo packet has been + * received from device + * + * @event onNodeInfoPacket + */ + public readonly onNodeInfoPacket: SimpleEventDispatcher< + Protobuf.Mesh.NodeInfo + > = new SimpleEventDispatcher< + Protobuf.Mesh.NodeInfo + >(); + + /** + * Fires when a new Channel message is received + * + * @event onChannelPacket + */ + public readonly onChannelPacket: SimpleEventDispatcher< + Protobuf.Channel.Channel + > = new SimpleEventDispatcher< + Protobuf.Channel.Channel + >(); + + /** + * Fires when a new Config message is received + * + * @event onConfigPacket + */ + public readonly onConfigPacket: SimpleEventDispatcher< + Protobuf.Config.Config + > = new SimpleEventDispatcher< + Protobuf.Config.Config + >(); + + /** + * Fires when a new ModuleConfig message is received + * + * @event onModuleConfigPacket + */ + public readonly onModuleConfigPacket: SimpleEventDispatcher< + Protobuf.ModuleConfig.ModuleConfig + > = new SimpleEventDispatcher< + Protobuf.ModuleConfig.ModuleConfig + >(); + + /** + * Fires when a new MeshPacket message containing a ATAK packet has been + * received from device + * + * @event onAtakPacket + */ + public readonly onAtakPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Text packet has been + * received from device + * + * @event onMessagePacket + */ + public readonly onMessagePacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Remote Hardware packet has + * been received from device + * + * @event onRemoteHardwarePacket + */ + public readonly onRemoteHardwarePacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Position packet has been + * received from device + * + * @event onPositionPacket + */ + public readonly onPositionPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a User packet has been + * received from device + * + * @event onUserPacket + */ + public readonly onUserPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Routing packet has been + * received from device + * + * @event onRoutingPacket + */ + public readonly onRoutingPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when the device receives a Metadata packet + * + * @event onDeviceMetadataPacket + */ + public readonly onDeviceMetadataPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when the device receives a Canned Message Module message packet + * + * @event onCannedMessageModulePacket + */ + public readonly onCannedMessageModulePacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Waypoint packet has been + * received from device + * + * @event onWaypointPacket + */ + public readonly onWaypointPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing an Audio packet has been + * received from device + * + * @event onAudioPacket + */ + public readonly onAudioPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Detection Sensor packet has been + * received from device + * + * @event onDetectionSensorPacket + */ + public readonly onDetectionSensorPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Ping packet has been + * received from device + * + * @event onPingPacket + */ + public readonly onPingPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a IP Tunnel packet has been + * received from device + * + * @event onIpTunnelPacket + */ + public readonly onIpTunnelPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Paxcounter packet has been + * received from device + * + * @event onPaxcounterPacket + */ + public readonly onPaxcounterPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Serial packet has been + * received from device + * + * @event onSerialPacket + */ + public readonly onSerialPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Store and Forward packet + * has been received from device + * + * @event onStoreForwardPacket + */ + public readonly onStoreForwardPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Store and Forward packet + * has been received from device + * + * @event onRangeTestPacket + */ + public readonly onRangeTestPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Telemetry packet has been + * received from device + * + * @event onTelemetryPacket + */ + public readonly onTelemetryPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a ZPS packet has been + * received from device + * + * @event onZPSPacket + */ + public readonly onZpsPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Simulator packet has been + * received from device + * + * @event onSimulatorPacket + */ + public readonly onSimulatorPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Trace Route packet has been + * received from device + * + * @event onTraceRoutePacket + */ + public readonly onTraceRoutePacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Neighbor Info packet has been + * received from device + * + * @event onNeighborInfoPacket + */ + public readonly onNeighborInfoPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing an ATAK packet has been + * received from device + * + * @event onAtakPluginPacket + */ + public readonly onAtakPluginPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Map Report packet has been + * received from device + * + * @event onMapReportPacket + */ + public readonly onMapReportPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing a Private packet has been + * received from device + * + * @event onPrivatePacket + */ + public readonly onPrivatePacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when a new MeshPacket message containing an ATAK Forwarder packet has been + * received from device + * + * @event onAtakForwarderPacket + */ + public readonly onAtakForwarderPacket: SimpleEventDispatcher< + PacketMetadata + > = new SimpleEventDispatcher< + PacketMetadata + >(); + + /** + * Fires when the devices connection or configuration status changes + * + * @event onDeviceStatus + */ + public readonly onDeviceStatus: SimpleEventDispatcher< + Types.DeviceStatusEnum + > = new SimpleEventDispatcher< + Types.DeviceStatusEnum + >(); + + /** + * Fires when a new FromRadio message containing a LogRecord packet has been + * received from device + * + * @event onLogRecord + */ + public readonly onLogRecord: SimpleEventDispatcher< + Protobuf.Mesh.LogRecord + > = new SimpleEventDispatcher< + Protobuf.Mesh.LogRecord + >(); + + /** + * Fires when the device receives a meshPacket, returns a timestamp + * + * @event onMeshHeartbeat + */ + public readonly onMeshHeartbeat: SimpleEventDispatcher = + new SimpleEventDispatcher(); + + /** + * Outputs any debug log data (currently serial connections only) + * + * @event onDeviceDebugLog + */ + public readonly onDeviceDebugLog: SimpleEventDispatcher = + new SimpleEventDispatcher(); + + /** + * Outputs status of pending settings changes + * + * @event onpendingSettingsChange + */ + public readonly onPendingSettingsChange: SimpleEventDispatcher< + boolean + > = new SimpleEventDispatcher< + boolean + >(); + + /** + * Fires when a QueueStatus message is generated + * + * @event onQueueStatus + */ + public readonly onQueueStatus: SimpleEventDispatcher< + Protobuf.Mesh.QueueStatus + > = new SimpleEventDispatcher< + Protobuf.Mesh.QueueStatus + >(); +} diff --git a/packages/core/src/utils/mod.ts b/packages/core/src/utils/mod.ts new file mode 100644 index 00000000..6c7d61d8 --- /dev/null +++ b/packages/core/src/utils/mod.ts @@ -0,0 +1,5 @@ +export { EventSystem } from "./eventSystem.ts"; +export { Queue } from "./queue.ts"; +export { Xmodem } from "./xmodem.ts"; +export { toDeviceStream } from "./transform/toDevice.ts"; +export { fromDeviceStream } from "./transform/fromDevice.ts"; diff --git a/packages/core/src/utils/queue.ts b/packages/core/src/utils/queue.ts new file mode 100644 index 00000000..4e2f26af --- /dev/null +++ b/packages/core/src/utils/queue.ts @@ -0,0 +1,119 @@ +import { SimpleEventDispatcher } from "ste-simple-events"; +import { fromBinary } from "@bufbuild/protobuf"; +import * as Protobuf from "@meshtastic/protobufs"; +import type { PacketError, QueueItem } from "../types.ts"; + +export class Queue { + private queue: QueueItem[] = []; + private lock = false; + private ackNotifier = new SimpleEventDispatcher(); + private errorNotifier = new SimpleEventDispatcher(); + private timeout: number; + + constructor() { + this.timeout = 60000; + } + + public getState(): QueueItem[] { + return this.queue; + } + + public clear(): void { + this.queue = []; + } + + public push(item: Omit): void { + const queueItem: QueueItem = { + ...item, + sent: false, + added: new Date(), + promise: new Promise((resolve, reject) => { + this.ackNotifier.subscribe((id) => { + if (item.id === id) { + this.remove(item.id); + resolve(id); + } + }); + this.errorNotifier.subscribe((e) => { + if (item.id === e.id) { + this.remove(item.id); + reject(e); + } + }); + setTimeout(() => { + if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) { + this.remove(item.id); + const decoded = fromBinary(Protobuf.Mesh.ToRadioSchema, item.data); + console.warn( + `Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`, + ); + + reject({ + id: item.id, + error: Protobuf.Mesh.Routing_Error.TIMEOUT, + }); + } + }, this.timeout); + }), + }; + this.queue.push(queueItem); + } + + public remove(id: number): void { + if (this.lock) { + setTimeout(() => this.remove(id), 100); + return; + } + this.queue = this.queue.filter((item) => item.id !== id); + } + + public processAck(id: number): void { + this.ackNotifier.dispatch(id); + } + + public processError(e: PacketError): void { + console.error( + `Error received for packet ${e.id}: ${ + Protobuf.Mesh.Routing_Error[e.error] + }`, + ); + this.errorNotifier.dispatch(e); + } + + public wait(id: number): Promise { + const queueItem = this.queue.find((qi) => qi.id === id); + if (!queueItem) { + throw new Error("Packet does not exist"); + } + return queueItem.promise; + } + + public async processQueue( + outputStream: WritableStream, + ): Promise { + if (this.lock) { + return; + } + + this.lock = true; + const writer = outputStream.getWriter(); + + try { + while (this.queue.filter((p) => !p.sent).length > 0) { + const item = this.queue.filter((p) => !p.sent)[0]; + if (item) { + await new Promise((resolve) => setTimeout(resolve, 200)); + try { + await writer.write(item.data); + item.sent = true; + } catch (error) { + console.error(`Error sending packet ${item.id}`, error); + } + } + } + } finally { + writer.releaseLock(); + this.lock = false; + } + } +} diff --git a/packages/core/src/utils/transform/decodePacket.ts b/packages/core/src/utils/transform/decodePacket.ts new file mode 100644 index 00000000..60cecc22 --- /dev/null +++ b/packages/core/src/utils/transform/decodePacket.ts @@ -0,0 +1,222 @@ +import { fromBinary } from "@bufbuild/protobuf"; +import type { DeviceOutput } from "../../types.ts"; +import { Constants, Protobuf, Types } from "../../../mod.ts"; +import type { MeshDevice } from "../../../mod.ts"; + +export const decodePacket = (device: MeshDevice) => + new WritableStream({ + write(chunk) { + switch (chunk.type) { + case "debug": { + break; + } + case "packet": { + const decodedMessage = fromBinary( + Protobuf.Mesh.FromRadioSchema, + chunk.data, + ); + device.events.onFromRadio.dispatch(decodedMessage); + + /** @todo Add map here when `all=true` gets fixed. */ + switch (decodedMessage.payloadVariant.case) { + case "packet": { + device.handleMeshPacket(decodedMessage.payloadVariant.value); + break; + } + + case "myInfo": { + device.events.onMyNodeInfo.dispatch( + decodedMessage.payloadVariant.value, + ); + device.log.info( + Types.Emitter[Types.Emitter.HandleFromRadio], + "📱 Received Node info for this device", + ); + break; + } + + case "nodeInfo": { + device.log.info( + Types.Emitter[Types.Emitter.HandleFromRadio], + `📱 Received Node Info packet for node: ${decodedMessage.payloadVariant.value.num}`, + ); + + device.events.onNodeInfoPacket.dispatch( + decodedMessage.payloadVariant.value, + ); + + //TODO: HERE + if (decodedMessage.payloadVariant.value.position) { + device.events.onPositionPacket.dispatch({ + id: decodedMessage.id, + rxTime: new Date(), + from: decodedMessage.payloadVariant.value.num, + to: decodedMessage.payloadVariant.value.num, + type: "direct", + channel: Types.ChannelNumber.Primary, + data: decodedMessage.payloadVariant.value.position, + }); + } + + //TODO: HERE + if (decodedMessage.payloadVariant.value.user) { + device.events.onUserPacket.dispatch({ + id: decodedMessage.id, + rxTime: new Date(), + from: decodedMessage.payloadVariant.value.num, + to: decodedMessage.payloadVariant.value.num, + type: "direct", + channel: Types.ChannelNumber.Primary, + data: decodedMessage.payloadVariant.value.user, + }); + } + break; + } + + case "config": { + if (decodedMessage.payloadVariant.value.payloadVariant.case) { + device.log.trace( + Types.Emitter[Types.Emitter.HandleFromRadio], + `💾 Received Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, + ); + } else { + device.log.warn( + Types.Emitter[Types.Emitter.HandleFromRadio], + `⚠️ Received Config packet of variant: ${"UNK"}`, + ); + } + + device.events.onConfigPacket.dispatch( + decodedMessage.payloadVariant.value, + ); + break; + } + + case "logRecord": { + device.log.trace( + Types.Emitter[Types.Emitter.HandleFromRadio], + "Received onLogRecord", + ); + device.events.onLogRecord.dispatch( + decodedMessage.payloadVariant.value, + ); + break; + } + + case "configCompleteId": { + if (decodedMessage.payloadVariant.value !== device.configId) { + device.log.error( + Types.Emitter[Types.Emitter.HandleFromRadio], + `❌ Invalid config id received from device, expected ${device.configId} but received ${decodedMessage.payloadVariant.value}`, + ); + } + + device.log.info( + Types.Emitter[Types.Emitter.HandleFromRadio], + `⚙️ Valid config id received from device: ${device.configId}`, + ); + + device.updateDeviceStatus( + Types.DeviceStatusEnum.DeviceConfigured, + ); + break; + } + + case "rebooted": { + device.configure().catch(() => { + // TODO: FIX, workaround for `wantConfigId` not getting acks. + }); + break; + } + + case "moduleConfig": { + if (decodedMessage.payloadVariant.value.payloadVariant.case) { + device.log.trace( + Types.Emitter[Types.Emitter.HandleFromRadio], + `💾 Received Module Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, + ); + } else { + device.log.warn( + Types.Emitter[Types.Emitter.HandleFromRadio], + "⚠️ Received Module Config packet of variant: UNK", + ); + } + + device.events.onModuleConfigPacket.dispatch( + decodedMessage.payloadVariant.value, + ); + break; + } + + case "channel": { + device.log.trace( + Types.Emitter[Types.Emitter.HandleFromRadio], + `🔐 Received Channel: ${decodedMessage.payloadVariant.value.index}`, + ); + + device.events.onChannelPacket.dispatch( + decodedMessage.payloadVariant.value, + ); + break; + } + + case "queueStatus": { + device.log.trace( + Types.Emitter[Types.Emitter.HandleFromRadio], + `🚧 Received Queue Status: ${decodedMessage.payloadVariant.value}`, + ); + + device.events.onQueueStatus.dispatch( + decodedMessage.payloadVariant.value, + ); + break; + } + + case "xmodemPacket": { + device.xModem.handlePacket(decodedMessage.payloadVariant.value); + break; + } + + case "metadata": { + if ( + Number.parseFloat( + decodedMessage.payloadVariant.value.firmwareVersion, + ) < Constants.minFwVer + ) { + device.log.fatal( + Types.Emitter[Types.Emitter.HandleFromRadio], + `Device firmware outdated. Min supported: ${Constants.minFwVer} got : ${decodedMessage.payloadVariant.value.firmwareVersion}`, + ); + } + device.log.debug( + Types.Emitter[Types.Emitter.GetMetadata], + "🏷️ Received metadata packet", + ); + + device.events.onDeviceMetadataPacket.dispatch({ + id: decodedMessage.id, + rxTime: new Date(), + from: 0, + to: 0, + type: "direct", + channel: Types.ChannelNumber.Primary, + data: decodedMessage.payloadVariant.value, + }); + break; + } + + case "mqttClientProxyMessage": { + break; + } + + default: { + device.log.warn( + Types.Emitter[Types.Emitter.HandleFromRadio], + `⚠️ Unhandled payload variant: ${decodedMessage.payloadVariant.case}`, + ); + } + } + } + } + }, + }); diff --git a/packages/core/src/utils/transform/fromDevice.ts b/packages/core/src/utils/transform/fromDevice.ts new file mode 100644 index 00000000..98e66ac3 --- /dev/null +++ b/packages/core/src/utils/transform/fromDevice.ts @@ -0,0 +1,73 @@ +import type { DeviceOutput } from "../../types.ts"; + +export const fromDeviceStream: () => TransformStream = + ( + // onReleaseEvent: SimpleEventDispatcher, + ) => { + let byteBuffer = new Uint8Array([]); + const textDecoder = new TextDecoder(); + return new TransformStream({ + transform(chunk: Uint8Array, controller): void { + // onReleaseEvent.subscribe(() => { + // controller.terminate(); + // }); + byteBuffer = new Uint8Array([...byteBuffer, ...chunk]); + let processingExhausted = false; + while (byteBuffer.length !== 0 && !processingExhausted) { + const framingIndex = byteBuffer.findIndex((byte) => byte === 0x94); + const framingByte2 = byteBuffer[framingIndex + 1]; + if (framingByte2 === 0xc3) { + if (byteBuffer.subarray(0, framingIndex).length) { + controller.enqueue({ + type: "debug", + data: textDecoder.decode(byteBuffer.subarray(0, framingIndex)), + }); + byteBuffer = byteBuffer.subarray(framingIndex); + } + + const msb = byteBuffer[2]; + const lsb = byteBuffer[3]; + + if ( + msb !== undefined && + lsb !== undefined && + byteBuffer.length >= 4 + (msb << 8) + lsb + ) { + const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb); + + const malformedDetectorIndex = packet.findIndex( + (byte) => byte === 0x94, + ); + if ( + malformedDetectorIndex !== -1 && + packet[malformedDetectorIndex + 1] === 0xc3 + ) { + console.warn( + `⚠️ Malformed packet found, discarding: ${ + byteBuffer + .subarray(0, malformedDetectorIndex - 1) + .toString() + }`, + ); + + byteBuffer = byteBuffer.subarray(malformedDetectorIndex); + } else { + byteBuffer = byteBuffer.subarray(3 + (msb << 8) + lsb + 1); + + controller.enqueue({ + type: "packet", + data: packet, + }); + } + } else { + /** Only partioal message in buffer, wait for the rest */ + processingExhausted = true; + } + } else { + /** Message not complete, only 1 byte in buffer */ + processingExhausted = true; + } + } + }, + }); + }; diff --git a/packages/core/src/utils/transform/toDevice.ts b/packages/core/src/utils/transform/toDevice.ts new file mode 100644 index 00000000..ef8e2203 --- /dev/null +++ b/packages/core/src/utils/transform/toDevice.ts @@ -0,0 +1,16 @@ +/** + * Pads packets with appropriate framing information before writing to the output stream. + */ +export const toDeviceStream: TransformStream = + new TransformStream({ + transform(chunk: Uint8Array, controller): void { + const bufLen = chunk.length; + const header = new Uint8Array([ + 0x94, + 0xC3, + (bufLen >> 8) & 0xFF, + bufLen & 0xFF, + ]); + controller.enqueue(new Uint8Array([...header, ...chunk])); + }, + }); diff --git a/packages/core/src/utils/xmodem.ts b/packages/core/src/utils/xmodem.ts new file mode 100644 index 00000000..47f2c3b1 --- /dev/null +++ b/packages/core/src/utils/xmodem.ts @@ -0,0 +1,135 @@ +import crc16ccitt from "crc/calculators/crc16ccitt"; +import { create, toBinary } from "@bufbuild/protobuf"; +import * as Protobuf from "@meshtastic/protobufs"; + +//if counter > 35 then reset counter/clear/error/reject promise +type XmodemProps = (toRadio: Uint8Array, id?: number) => Promise; + +export class Xmodem { + private sendRaw: XmodemProps; + private rxBuffer: Uint8Array[]; + private txBuffer: Uint8Array[]; + private textEncoder: TextEncoder; + private counter: number; + + constructor(sendRaw: XmodemProps) { + this.sendRaw = sendRaw; + this.rxBuffer = []; + this.txBuffer = []; + this.textEncoder = new TextEncoder(); + this.counter = 0; + } + + async downloadFile(filename: string): Promise { + return await this.sendCommand( + Protobuf.Xmodem.XModem_Control.STX, + this.textEncoder.encode(filename), + 0, + ); + } + + async uploadFile(filename: string, data: Uint8Array): Promise { + for (let i = 0; i < data.length; i += 128) { + this.txBuffer.push(data.slice(i, i + 128)); + } + + return await this.sendCommand( + Protobuf.Xmodem.XModem_Control.SOH, + this.textEncoder.encode(filename), + 0, + ); + } + + async sendCommand( + command: Protobuf.Xmodem.XModem_Control, + buffer?: Uint8Array, + sequence?: number, + crc16?: number, + ): Promise { + const toRadio = create(Protobuf.Mesh.ToRadioSchema, { + payloadVariant: { + case: "xmodemPacket", + value: { + buffer, + control: command, + seq: sequence, + crc16: crc16, + }, + }, + }); + return await this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)); + } + + async handlePacket(packet: Protobuf.Xmodem.XModem): Promise { + await new Promise((resolve) => setTimeout(resolve, 100)); + + switch (packet.control) { + case Protobuf.Xmodem.XModem_Control.NUL: { + // nothing + break; + } + case Protobuf.Xmodem.XModem_Control.SOH: { + this.counter = packet.seq; + if (this.validateCrc16(packet)) { + this.rxBuffer[this.counter] = packet.buffer; + return this.sendCommand(Protobuf.Xmodem.XModem_Control.ACK); + } + return await this.sendCommand( + Protobuf.Xmodem.XModem_Control.NAK, + undefined, + packet.seq, + ); + } + case Protobuf.Xmodem.XModem_Control.STX: { + break; + } + case Protobuf.Xmodem.XModem_Control.EOT: { + // end of transmission + break; + } + case Protobuf.Xmodem.XModem_Control.ACK: { + this.counter++; + if (this.txBuffer[this.counter - 1]) { + return this.sendCommand( + Protobuf.Xmodem.XModem_Control.SOH, + this.txBuffer[this.counter - 1], + this.counter, + crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()), + ); + } + if (this.counter === this.txBuffer.length + 1) { + return this.sendCommand(Protobuf.Xmodem.XModem_Control.EOT); + } + this.clear(); + break; + } + case Protobuf.Xmodem.XModem_Control.NAK: { + return this.sendCommand( + Protobuf.Xmodem.XModem_Control.SOH, + this.txBuffer[this.counter], + this.counter, + crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()), + ); + } + case Protobuf.Xmodem.XModem_Control.CAN: { + this.clear(); + break; + } + case Protobuf.Xmodem.XModem_Control.CTRLZ: { + break; + } + } + + return Promise.resolve(0); + } + + validateCrc16(packet: Protobuf.Xmodem.XModem): boolean { + return crc16ccitt(packet.buffer) === packet.crc16; + } + + clear() { + this.counter = 0; + this.rxBuffer = []; + this.txBuffer = []; + } +} diff --git a/packages/transport-deno/README.md b/packages/transport-deno/README.md new file mode 100644 index 00000000..1c5213f8 --- /dev/null +++ b/packages/transport-deno/README.md @@ -0,0 +1,27 @@ +# @meshtastic/transport-deno + +[![JSR](https://jsr.io/badges/@meshtastic/transport-deno)](https://jsr.io/@meshtastic/transport-deno) +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml) +[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js) +[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) +[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) + +## Overview + +`@meshtastic/transport-deno` Provides TCP transport (Deno) for Meshtastic +devices. Installation instructions are avaliable at +[JSR](https://jsr.io/@meshtastic/transport-deno) + +## Usage + +```ts +import { MeshDevice } from "@meshtastic/core"; +import { TransportDeno } from "@meshtastic/transport-deno"; + +const transport = await TransportDeno.create("10.10.0.57"); +const device = new MeshDevice(transport); +``` + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") diff --git a/packages/transport-deno/deno.json b/packages/transport-deno/deno.json new file mode 100644 index 00000000..50968ed6 --- /dev/null +++ b/packages/transport-deno/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@meshtastic/transport-deno", + "version": "0.1.1", + "exports": { + ".": "./mod.ts" + } +} diff --git a/packages/transport-deno/mod.ts b/packages/transport-deno/mod.ts new file mode 100644 index 00000000..55a63346 --- /dev/null +++ b/packages/transport-deno/mod.ts @@ -0,0 +1 @@ +export { TransportDeno } from "./src/transport.ts"; diff --git a/packages/transport-deno/src/transport.ts b/packages/transport-deno/src/transport.ts new file mode 100644 index 00000000..c5ef2bb7 --- /dev/null +++ b/packages/transport-deno/src/transport.ts @@ -0,0 +1,32 @@ +import { Utils } from "@meshtastic/core"; +import type { Types } from "@meshtastic/core"; + +export class TransportDeno implements Types.Transport { + private _toDevice: WritableStream; + private _fromDevice: ReadableStream; + + public static async create(hostname: string): Promise { + const connection = await Deno.connect({ + hostname, + port: 4403, + }); + return new TransportDeno(connection); + } + + constructor(connection: Deno.Conn) { + Utils.toDeviceStream.readable.pipeTo(connection.writable); + + this._toDevice = Utils.toDeviceStream.writable; + this._fromDevice = connection.readable.pipeThrough( + Utils.fromDeviceStream(), + ); + } + + get toDevice(): WritableStream { + return this._toDevice; + } + + get fromDevice(): ReadableStream { + return this._fromDevice; + } +} diff --git a/packages/transport-http/README.md b/packages/transport-http/README.md new file mode 100644 index 00000000..560b44b4 --- /dev/null +++ b/packages/transport-http/README.md @@ -0,0 +1,27 @@ +# @meshtastic/transport-http + +[![JSR](https://jsr.io/badges/@meshtastic/transport-http)](https://jsr.io/@meshtastic/transport-http) +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml) +[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js) +[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) +[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) + +## Overview + +`@meshtastic/transport-http` Provides HTTP(S) transport for Meshtastic devices. +Installation instructions are available at +[JSR](https://jsr.io/@meshtastic/transport-http) + +## Usage + +```ts +import { MeshDevice } from "@meshtastic/core"; +import { TransportHTTP } from "@meshtastic/transport-http"; + +const transport = await TransportHTTP.create("10.10.0.57"); +const device = new MeshDevice(transport); +``` + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") diff --git a/packages/transport-http/deno.json b/packages/transport-http/deno.json new file mode 100644 index 00000000..1bcb8348 --- /dev/null +++ b/packages/transport-http/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@meshtastic/transport-http", + "version": "0.2.1", + "exports": { + ".": "./mod.ts" + } +} diff --git a/packages/transport-http/mod.ts b/packages/transport-http/mod.ts new file mode 100644 index 00000000..20af396c --- /dev/null +++ b/packages/transport-http/mod.ts @@ -0,0 +1 @@ +export { TransportHTTP } from "./src/transport.ts"; diff --git a/packages/transport-http/src/transport.ts b/packages/transport-http/src/transport.ts new file mode 100644 index 00000000..03f578aa --- /dev/null +++ b/packages/transport-http/src/transport.ts @@ -0,0 +1,89 @@ +import type { Types } from "@meshtastic/core"; + +export class TransportHTTP implements Types.Transport { + private _toDevice: WritableStream; + private _fromDevice: ReadableStream; + private url: string; + private receiveBatchRequests: boolean; + private fetchInterval: number; + + public static async create( + address: string, + tls?: boolean, + ): Promise { + const connectionUrl = `${tls ? "https" : "http"}://${address}`; + await fetch(`${connectionUrl}/json/report`); + await Promise.resolve(); + return new TransportHTTP(connectionUrl); + } + + constructor(url: string) { + this.url = url; + this.receiveBatchRequests = false; + this.fetchInterval = 3000; + + this._toDevice = new WritableStream({ + write: async (chunk) => { + await this.writeToRadio(chunk); + }, + }); + + let controller: ReadableStreamDefaultController; + + this._fromDevice = new ReadableStream({ + start: (ctrl) => { + controller = ctrl; + }, + }); + + setInterval(async () => { + await this.readFromRadio(controller); + }, this.fetchInterval); + } + + private async readFromRadio( + controller: ReadableStreamDefaultController, + ): Promise { + let readBuffer = new ArrayBuffer(1); + while (readBuffer.byteLength > 0) { + const response = await fetch( + `${this.url}/api/v1/fromradio?all=${ + this.receiveBatchRequests ? "true" : "false" + }`, + { + method: "GET", + headers: { + Accept: "application/x-protobuf", + }, + }, + ); + + readBuffer = await response.arrayBuffer(); + + if (readBuffer.byteLength > 0) { + controller.enqueue({ + type: "packet", + data: new Uint8Array(readBuffer), + }); + } + } + } + + private async writeToRadio(data: Uint8Array): Promise { + await fetch(`${this.url}/api/v1/toradio`, { + method: "PUT", + headers: { + "Content-Type": "application/x-protobuf", + }, + body: data, + }); + } + + get toDevice(): WritableStream { + return this._toDevice; + } + + get fromDevice(): ReadableStream { + return this._fromDevice; + } +} diff --git a/packages/transport-web-bluetooth/README.md b/packages/transport-web-bluetooth/README.md new file mode 100644 index 00000000..ab1af911 --- /dev/null +++ b/packages/transport-web-bluetooth/README.md @@ -0,0 +1,34 @@ +# @meshtastic/transport-web-bluetooth + +[![JSR](https://jsr.io/badges/@meshtastic/transport-web-bluetooth)](https://jsr.io/@meshtastic/transport-web-bluetooth) +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml) +[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js) +[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) +[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) + +## Overview + +`@meshtastic/transport-web-bluetooth` Provides Web Bluetooth transport for +Meshtastic devices. Installation instructions are available at +[JSR](https://jsr.io/@meshtastic/transport-web-bluetooth) + +## Usage + +```ts +import { MeshDevice } from "@meshtastic/core"; +import { TransportWebBluetooth } from "@meshtastic/transport-web-bluetooth"; + +const transport = await TransportWebBluetooth.create(); +const device = new MeshDevice(transport); +``` + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") + +### Compatibility + +The Web Bluetooth API's have limited support in browsers, compatibility is +represented in the matrix below. + +![Web Bluetooth compatability matrix](https://caniuse.bitsofco.de/image/web-bluetooth.png) diff --git a/packages/transport-web-bluetooth/deno.json b/packages/transport-web-bluetooth/deno.json new file mode 100644 index 00000000..6aab4b7b --- /dev/null +++ b/packages/transport-web-bluetooth/deno.json @@ -0,0 +1,15 @@ +{ + "name": "@meshtastic/transport-web-bluetooth", + "version": "0.1.2", + "exports": { + ".": "./mod.ts" + }, + "imports": { + "@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20" + }, + "compilerOptions": { + "types": [ + "@types/web-bluetooth" + ] + } +} diff --git a/packages/transport-web-bluetooth/mod.ts b/packages/transport-web-bluetooth/mod.ts new file mode 100644 index 00000000..5aebcf2f --- /dev/null +++ b/packages/transport-web-bluetooth/mod.ts @@ -0,0 +1 @@ +export { TransportWebBluetooth } from "./src/transport.ts"; diff --git a/packages/transport-web-bluetooth/src/transport.ts b/packages/transport-web-bluetooth/src/transport.ts new file mode 100644 index 00000000..d2b9dd61 --- /dev/null +++ b/packages/transport-web-bluetooth/src/transport.ts @@ -0,0 +1,134 @@ +import type { Types } from "@meshtastic/core"; + +export class TransportWebBluetooth implements Types.Transport { + private _toDevice: WritableStream; + private _fromDevice: ReadableStream; + private _fromDeviceController?: ReadableStreamDefaultController< + Types.DeviceOutput + >; + private _isFirstWrite = true; + + private toRadioCharacteristic: BluetoothRemoteGATTCharacteristic; + private fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic; + private fromNumCharacteristic: BluetoothRemoteGATTCharacteristic; + + static ToRadioUuid = "f75c76d2-129e-4dad-a1dd-7866124401e7"; + static FromRadioUuid = "2c55e69e-4993-11ed-b878-0242ac120002"; + static FromNumUuid = "ed9da18c-a800-4f66-a670-aa7547e34453"; + static ServiceUuid = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"; + + public static async create(): Promise { + const device = await navigator.bluetooth.requestDevice({ + filters: [{ services: [this.ServiceUuid] }], + }); + return await this.prepareConnection(device); + } + + public static async createFromDevice( + device: BluetoothDevice, + ): Promise { + return await this.prepareConnection(device); + } + + public static async prepareConnection( + device: BluetoothDevice, + ): Promise { + const gattServer = await device.gatt?.connect(); + + if (!gattServer) { + throw new Error("Failed to connect to GATT server"); + } + + const service = await gattServer.getPrimaryService(this.ServiceUuid); + + const toRadioCharacteristic = await service.getCharacteristic( + this.ToRadioUuid, + ); + const fromRadioCharacteristic = await service.getCharacteristic( + this.FromRadioUuid, + ); + const fromNumCharacteristic = await service.getCharacteristic( + this.FromNumUuid, + ); + + if ( + !toRadioCharacteristic || !fromRadioCharacteristic || + !fromNumCharacteristic + ) { + throw new Error("Failed to find required characteristics"); + } + + console.log("Connected to device", device.name); + + return new TransportWebBluetooth( + toRadioCharacteristic, + fromRadioCharacteristic, + fromNumCharacteristic, + ); + } + + constructor( + toRadioCharacteristic: BluetoothRemoteGATTCharacteristic, + fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic, + fromNumCharacteristic: BluetoothRemoteGATTCharacteristic, + ) { + this.toRadioCharacteristic = toRadioCharacteristic; + this.fromRadioCharacteristic = fromRadioCharacteristic; + this.fromNumCharacteristic = fromNumCharacteristic; + + this._fromDevice = new ReadableStream({ + start: (ctrl) => { + this._fromDeviceController = ctrl; + }, + }); + + this._toDevice = new WritableStream({ + write: async (chunk) => { + await this.toRadioCharacteristic.writeValue(chunk); + + if (this._isFirstWrite && this._fromDeviceController) { + this._isFirstWrite = false; + setTimeout(() => { + this.readFromRadio(this._fromDeviceController!); + }, 50); + } + }, + }); + + this.fromNumCharacteristic.addEventListener( + "characteristicvaluechanged", + () => { + if (this._fromDeviceController) { + this.readFromRadio(this._fromDeviceController); + } + }, + ); + + this.fromNumCharacteristic.startNotifications(); + } + + get toDevice(): WritableStream { + return this._toDevice; + } + + get fromDevice(): ReadableStream { + return this._fromDevice; + } + + protected async readFromRadio( + controller: ReadableStreamDefaultController, + ): Promise { + let hasMoreData = true; + while (hasMoreData && this.fromRadioCharacteristic) { + const value = await this.fromRadioCharacteristic.readValue(); + if (value.byteLength === 0) { + hasMoreData = false; + continue; + } + controller.enqueue({ + type: "packet", + data: new Uint8Array(value.buffer), + }); + } + } +} diff --git a/packages/transport-web-serial/README.md b/packages/transport-web-serial/README.md new file mode 100644 index 00000000..81c08b99 --- /dev/null +++ b/packages/transport-web-serial/README.md @@ -0,0 +1,34 @@ +# @meshtastic/transport-web-serial + +[![JSR](https://jsr.io/badges/@meshtastic/transport-web-serial)](https://jsr.io/@meshtastic/transport-web-serial) +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml) +[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js) +[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) +[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) + +## Overview + +`@meshtastic/transport-web-serial` Provides Web Serial transport for Meshtastic +devices. Installation instructions are avaliable at +[JSR](https://jsr.io/@meshtastic/transport-web-serial) + +## Usage + +```ts +import { MeshDevice } from "@meshtastic/core"; +import { TransportWebSerial } from "@meshtastic/transport-web-serial"; + +const transport = await TransportWebSerial.create(); +const device = new MeshDevice(transport); +``` + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") + +### Compatibility + +The Web Serial API's have limited support in browsers, compatibility is +represented in the matrix below. + +![Web Serial compatability matrix](https://caniuse.bitsofco.de/image/web-serial.png) diff --git a/packages/transport-web-serial/deno.json b/packages/transport-web-serial/deno.json new file mode 100644 index 00000000..45a90622 --- /dev/null +++ b/packages/transport-web-serial/deno.json @@ -0,0 +1,15 @@ +{ + "name": "@meshtastic/transport-web-serial", + "version": "0.2.1", + "exports": { + ".": "./mod.ts" + }, + "imports": { + "@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7" + }, + "compilerOptions": { + "types": [ + "@types/w3c-web-serial" + ] + } +} diff --git a/packages/transport-web-serial/mod.ts b/packages/transport-web-serial/mod.ts new file mode 100644 index 00000000..a914a57c --- /dev/null +++ b/packages/transport-web-serial/mod.ts @@ -0,0 +1 @@ +export { TransportWebSerial } from "./src/transport.ts"; diff --git a/packages/transport-web-serial/src/transport.ts b/packages/transport-web-serial/src/transport.ts new file mode 100644 index 00000000..0f445cbe --- /dev/null +++ b/packages/transport-web-serial/src/transport.ts @@ -0,0 +1,42 @@ +import { Utils } from "@meshtastic/core"; +import type { Types } from "@meshtastic/core"; + +export class TransportWebSerial implements Types.Transport { + private _toDevice: WritableStream; + private _fromDevice: ReadableStream; + + public static async create(baudRate?: number): Promise { + const port = await navigator.serial.requestPort(); + await port.open({ baudRate: baudRate || 115200 }); + return new TransportWebSerial(port); + } + + public static async createFromPort( + port: SerialPort, + baudRate?: number, + ): Promise { + await port.open({ baudRate: baudRate || 115200 }); + return new TransportWebSerial(port); + } + + constructor(connection: SerialPort) { + if (!connection.readable || !connection.writable) { + throw new Error("Stream not accessible"); + } + + Utils.toDeviceStream.readable.pipeTo(connection.writable); + + this._toDevice = Utils.toDeviceStream.writable; + this._fromDevice = connection.readable.pipeThrough( + Utils.fromDeviceStream(), + ); + } + + get toDevice(): WritableStream { + return this._toDevice; + } + + get fromDevice(): ReadableStream { + return this._fromDevice; + } +} diff --git a/packages/web/.npmrc b/packages/web/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/packages/web/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/packages/web/CONTRIBUTING_I18N_DEVELOPER_GUIDE.md b/packages/web/CONTRIBUTING_I18N_DEVELOPER_GUIDE.md new file mode 100644 index 00000000..d41ad088 --- /dev/null +++ b/packages/web/CONTRIBUTING_I18N_DEVELOPER_GUIDE.md @@ -0,0 +1,111 @@ +# i18n Developer Guide + +When developing new components, all user-facing text must be added as an i18n +key and rendered using our translation functions. This ensures your UI can be +translated into multiple languages. + +## Adding New i18n Keys + +### Search Before Creating + +Before adding a new key, please perform a quick search to see if one that fits +your needs already exists. Many common labels like "Save," "Cancel," "Name," +"Description," "Loading...," or "Error" are likely already present, especially +in the common.json namespace. Reusing existing keys prevents duplication and +ensures consistency across the application. Using your code editor's search +function across the /i18n/locales/en/ directory is an effective way to do this. + +### Key Naming and Structure Rules + +To maintain consistency and ease of use, please adhere to the following rules +when creating new keys in the JSON files. + +- **Keys are camelCase:** `exampleKey`, `anotherExampleKey`. +- **Avoid Deep Nesting:** One or two levels of nesting are acceptable for + grouping related keys (e.g., all labels for a specific menu). However, nesting + deeper than two levels should be avoided to maintain readability and ease of + use. + - **Good (1 level):** + ```json + "buttons": { + "save": "Save", + "cancel": "Cancel" + } + ``` + - **Acceptable (2 levels):** + ```json + "userMenu": { + "items": { + "profile": "Profile", + "settings": "Settings" + } + } + ``` + - **Avoid (3+ levels):** + ```json + "userMenu": { + "items": { + "actions": { + "viewProfile": "View Profile" + } + } + } + ``` +- **Organize for Retrieval, Not UI Layout:** Keys should be named logically for + easy retrieval, not to mirror the layout of your component. + +### Namespace Rules + +We use namespaces to organize keys. All source keys are added to the English +(`en`) files located at `/i18n/locales/en/`. Place your new keys in the +appropriate file based on these rules: + +- `common.json`: + - All button labels (`save`, `cancel`, `submit`, etc.). + - Any text that is repeated and used throughout the application (e.g., + "Loading...", "Error"). +- `ui.json`: + - Labels and text specific to a distinct UI element or view that isn't a + dialog or a config page. +- `dialog.json`: + - All text specific to modal dialogs (titles, body text, prompts). +- `messages.json`: + - Text specifically related to the messaging interface. +- `deviceConfig.json` & `moduleConfig.json`: + - Labels and descriptions for the settings on the Device and Module + configuration pages. + +## Using i18n Keys in Components + +We use the `useTranslation` hook from `react-i18next` to access the translation +function, `t`. + +### Default Namespaces + +Our i18next configuration has fallback namespaces configured which includes +`common`, `ui`, and `dialog`. This means you **do not** need to explicitly +specify these namespaces when calling the hook. The system will automatically +check these files for your key. + +For any keys in `common.json`, `ui.json`, or `dialog.json`, you can instantiate +the hook simply: + +```typescript +import { useTranslation } from "react-i18next"; + +// In your component +const { t } = useTranslation(["messages"]); + +// Usage +return

{t("someMessageLabel")}

; +``` + +You can also specify the namespace on a per-call basis using the options object. +This is useful if a component primarily uses a default namespace but needs a +single key from another. + +```typescript +const { t } = useTranslation(); + +return

{t("someMessageLabel", { ns: "messages" })}

; +``` diff --git a/packages/web/CONTRIBUTING_TRANSLATIONS.md b/packages/web/CONTRIBUTING_TRANSLATIONS.md new file mode 100644 index 00000000..66a7d646 --- /dev/null +++ b/packages/web/CONTRIBUTING_TRANSLATIONS.md @@ -0,0 +1,31 @@ +# Contributing Translations + +Thank you for your interest in making the Meshtastic Web Client accessible to a +global audience! Your translation efforts are greatly appreciated. + +## Our Translation Platform: Crowdin + +We manage all our translations through a platform called +[Crowdin](https://crowdin.com/). This allows for a collaborative and streamlined +translation process. All translation work should be done on our Crowdin project, +not directly in the code repository via Pull Requests. + +### How to Get Started + +1. **Create a Crowdin Account:** If you don't already have one, sign up for a + free account on Crowdin. +2. **Join Our Project:** Please ask for a link to our specific Crowdin project + on the Meshtastic Discord. +3. **Request Translator Role:** Once you have an account, join the Meshtastic + Discord and notify an admin in the `#web` channel. They will grant you the + necessary permissions to start translating. +4. **Start Translating:** Once you have your role, you can begin translating the + source labels into your native language directly on the Crowdin platform. + +### Language Activation + +A new language will only be added to the web client and appear in the language +picker once its translation is 100% complete on Crowdin. The repository +maintainers will handle this process once the milestone is reached. + +Thank you for helping us bring Meshtastic to more users around the world! diff --git a/LICENSE b/packages/web/LICENSE similarity index 100% rename from LICENSE rename to packages/web/LICENSE diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 00000000..b64adf57 --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,175 @@ +# Meshtastic Web + + + +[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/web/ci.yml?branch=main&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/web/actions/workflows/ci.yml) +[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/web)](https://cla-assistant.io/meshtastic/web) +[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) +[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) + +## Overview + +Official [Meshtastic](https://meshtastic.org) web interface, that can be hosted +or served from a node + +**[Hosted version](https://client.meshtastic.org)** + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image") + +## Progress Web App Support (PWA) + +Meshtastic Web Client now includes Progressive Web App (PWA) functionality, +allowing users to: + +- Install the app on desktop and mobile devices +- Access the interface offline +- Receive updates automatically +- Experience faster load times with caching + +To install as a PWA: + +- On desktop: Look for the install icon in your browser's address bar +- On mobile: Use "Add to Home Screen" option in your browser menu + +PWA functionality works with both the hosted version and self-hosted instances. + +## Self-host + +The client can be self hosted using the precompiled container images with an OCI +compatible runtime such as [Docker](https://www.docker.com/) or +[Podman](https://podman.io/). The base image used is +[Nginx 1.27](https://hub.docker.com/_/nginx) + +```bash +# With Docker +docker run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web + +#With Podman +podman run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web +``` + +## Release Schedule + +Our release process follows these guidelines: + +- **Versioning:** We use Semantic Versioning (`Major.Minor.Patch`). +- **Stable Releases:** Published around the beginning of each month (e.g., + `v2.3.4`). +- **Pre-releases:** A pre-release is typically issued mid-month for testing and + early adoption. +- **Nightly Builds:** An experimental Docker image containing the latest + cutting-edge features and fixes is automatically built nightly from the `main` + branch. + +### Nightly Builds + +```bash +# With Docker +docker run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web:nightly +#With Podman +podman run -d -p 8080:8080 --restart always --name Meshtastic-Web ghcr.io/meshtastic/web:nightly +``` + +> [!WARNING] +> +> - Nightly builds represent the latest development state and may contain +> breaking changes +> - These builds undergo automated testing but may be less stable than tagged +> release versions +> - Not recommended for production environments unless you are actively testing +> new features +> - No guarantee of backward compatibility between nightly builds + +#### Version Information + +Each nightly build is tagged with: + +- The nightly tag for the latest build +- A specific SHA for build reproducibility + +### Feedback + +If you encounter any issues with nightly builds, please report them in our +[issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps +improve the stability of future releases + +## Development & Building + +You'll need to download the package manager used with this repo. You can install +it by visiting [deno.com](https://deno.com/) and following the installation +instructions listed on the home page. + +### Development + +Install the dependencies. + +```bash +deno i +``` + +Start the development server: + +```bash +deno task dev +``` + +### Building and Packaging + +Build the project: + +```bash +deno task build +``` + +GZip the output: + +```bash +deno task package +``` + +### Why Deno? + +Meshtastic Web uses Deno as its development platform for several compelling +reasons: + +- **Built-in Security**: Deno's security-first approach requires explicit + permissions for file, network, and environment access, reducing vulnerability + risks. +- **TypeScript Support**: Native TypeScript support without additional + configuration, enhancing code quality and developer experience. +- **Modern JavaScript**: First-class support for ESM imports, top-level await, + and other modern JavaScript features. +- **Simplified Tooling**: Built-in formatter, linter, test runner, and bundler + eliminate the need for multiple third-party tools. +- **Reproducible Builds**: Lockfile ensures consistent builds across all + environments. +- **Web Standard APIs**: Uses browser-compatible APIs, making code more portable + between server and client environments. + +### Contributing + +We welcome contributions! Here’s how the deployment flow works for pull +requests: + +- **Preview Deployments:**\ + Every pull request automatically generates a preview deployment on Vercel. + This allows you and reviewers to easily preview changes before merging. + +- **Staging Environment (`client-test`):**\ + Once your PR is merged, your changes will be available on our staging site: + [client-test.meshtastic.org](https://client-test.meshtastic.org/).\ + This environment supports rapid feature iteration and testing without + impacting the production site. + +- **Production Releases:**\ + At regular intervals, stable and fully tested releases are promoted to our + production site: [client.meshtastic.org](https://client.meshtastic.org/).\ + This is the primary interface used by the public to connect with their + Meshtastic nodes. + +Please review our +[Contribution Guidelines](https://github.com/meshtastic/web/blob/main/CONTRIBUTING.md) +before submitting a pull request. We appreciate your help in making the project +better! diff --git a/crowdin.yml b/packages/web/crowdin.yml similarity index 60% rename from crowdin.yml rename to packages/web/crowdin.yml index 14d994ec..0ceed174 100644 --- a/crowdin.yml +++ b/packages/web/crowdin.yml @@ -6,5 +6,5 @@ base_url: "https://meshtastic.crowdin.com/api/v2" preserve_hierarchy: true files: - - source: "/src/i18n/locales/en/**/*.json" - translation: "/src/i18n/locales/%locale%/%original_file_name%" + - source: "/i18n/locales/en/*.json" + translation: "/i18n/locales/%locale%/%original_file_name%" diff --git a/packages/web/deno.json b/packages/web/deno.json new file mode 100644 index 00000000..a3da671b --- /dev/null +++ b/packages/web/deno.json @@ -0,0 +1,58 @@ +{ + "imports": { + "@app/": "./src/", + "@public/": "./public/", + "@pages/": "./src/pages/", + "@components/": "./src/components/", + "@core/": "./src/core/", + "@layouts/": "./src/layouts/", + "@std/path": "jsr:@std/path@^1.1.0" + }, + "tasks": { + "build": "vite build", + "build:analyze": "BUNDLE_ANALYZE=true deno task build", + "lint": "deno lint src/", + "lint:fix": "deno lint --fix src/", + "format": "deno fmt src/", + "dev": "deno task dev:ui", + "dev:ui": "VITE_APP_VERSION=development deno run -A npm:vite dev", + "test": "deno run -A npm:vitest", + "check": "deno check", + "preview": "deno run -A npm:vite preview", + "generate:routes": "deno run -A npm:@tanstack/router-cli generate --outDir src/ routes --rootRoutePath /", + "package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ." + }, + "include": ["src", "./vite-env.d.ts"], + "compilerOptions": { + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext", + "deno.window", + "deno.ns" + ], + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "strictNullChecks": true, + "types": [ + "vite/client", + "node", + "npm:@types/w3c-web-serial", + "npm:@types/web-bluetooth" + ], + "strictPropertyInitialization": false + }, + + "exclude": [ + "routeTree.gen.ts", + "node_modules/", + "dist", + "build", + "coverage", + "out", + ".vscode-test" + ] +} diff --git a/index.html b/packages/web/index.html similarity index 94% rename from index.html rename to packages/web/index.html index 2a9228d5..517ec914 100644 --- a/index.html +++ b/packages/web/index.html @@ -7,7 +7,7 @@ - + + + + Created with Fabric.js 4.6.0 + + + + + + + + + + + diff --git a/packages/web/public/Logo_Black.svg b/packages/web/public/Logo_Black.svg new file mode 100644 index 00000000..3568d300 --- /dev/null +++ b/packages/web/public/Logo_Black.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/packages/web/public/Logo_White.svg b/packages/web/public/Logo_White.svg new file mode 100644 index 00000000..7c5417ed --- /dev/null +++ b/packages/web/public/Logo_White.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/public/apple-touch-icon.png b/packages/web/public/apple-touch-icon.png similarity index 100% rename from public/apple-touch-icon.png rename to packages/web/public/apple-touch-icon.png diff --git a/packages/web/public/chirpy.svg b/packages/web/public/chirpy.svg new file mode 100644 index 00000000..8769215f --- /dev/null +++ b/packages/web/public/chirpy.svg @@ -0,0 +1,64 @@ + + + diff --git a/packages/web/public/devices/diy.svg b/packages/web/public/devices/diy.svg new file mode 100644 index 00000000..2a364fd5 --- /dev/null +++ b/packages/web/public/devices/diy.svg @@ -0,0 +1,2233 @@ + + + diff --git a/packages/web/public/devices/heltec-ht62-esp32c3-sx1262.svg b/packages/web/public/devices/heltec-ht62-esp32c3-sx1262.svg new file mode 100644 index 00000000..ff244831 --- /dev/null +++ b/packages/web/public/devices/heltec-ht62-esp32c3-sx1262.svg @@ -0,0 +1,2604 @@ + + + diff --git a/packages/web/public/devices/heltec-mesh-node-t114-case.svg b/packages/web/public/devices/heltec-mesh-node-t114-case.svg new file mode 100644 index 00000000..895c6a39 --- /dev/null +++ b/packages/web/public/devices/heltec-mesh-node-t114-case.svg @@ -0,0 +1,271 @@ + + + diff --git a/packages/web/public/devices/heltec-mesh-node-t114.svg b/packages/web/public/devices/heltec-mesh-node-t114.svg new file mode 100644 index 00000000..d679a4b4 --- /dev/null +++ b/packages/web/public/devices/heltec-mesh-node-t114.svg @@ -0,0 +1,398 @@ + + + diff --git a/packages/web/public/devices/heltec-v3-case.svg b/packages/web/public/devices/heltec-v3-case.svg new file mode 100644 index 00000000..2f772c5b --- /dev/null +++ b/packages/web/public/devices/heltec-v3-case.svg @@ -0,0 +1,74 @@ + + + diff --git a/packages/web/public/devices/heltec-v3.svg b/packages/web/public/devices/heltec-v3.svg new file mode 100644 index 00000000..1c38cdcc --- /dev/null +++ b/packages/web/public/devices/heltec-v3.svg @@ -0,0 +1,1496 @@ + + + diff --git a/packages/web/public/devices/heltec-vision-master-e213.svg b/packages/web/public/devices/heltec-vision-master-e213.svg new file mode 100644 index 00000000..1f128233 --- /dev/null +++ b/packages/web/public/devices/heltec-vision-master-e213.svg @@ -0,0 +1,149 @@ + + + diff --git a/packages/web/public/devices/heltec-vision-master-e290.svg b/packages/web/public/devices/heltec-vision-master-e290.svg new file mode 100644 index 00000000..dc64e35e --- /dev/null +++ b/packages/web/public/devices/heltec-vision-master-e290.svg @@ -0,0 +1,221 @@ + + + diff --git a/packages/web/public/devices/heltec-vision-master-t190.svg b/packages/web/public/devices/heltec-vision-master-t190.svg new file mode 100644 index 00000000..0822ce3d --- /dev/null +++ b/packages/web/public/devices/heltec-vision-master-t190.svg @@ -0,0 +1,213 @@ + + + diff --git a/packages/web/public/devices/heltec-wireless-paper-V1_0.svg b/packages/web/public/devices/heltec-wireless-paper-V1_0.svg new file mode 100644 index 00000000..def8143e --- /dev/null +++ b/packages/web/public/devices/heltec-wireless-paper-V1_0.svg @@ -0,0 +1,510 @@ + + + diff --git a/packages/web/public/devices/heltec-wireless-paper.svg b/packages/web/public/devices/heltec-wireless-paper.svg new file mode 100644 index 00000000..def8143e --- /dev/null +++ b/packages/web/public/devices/heltec-wireless-paper.svg @@ -0,0 +1,510 @@ + + + diff --git a/packages/web/public/devices/heltec-wireless-tracker-V1-0.svg b/packages/web/public/devices/heltec-wireless-tracker-V1-0.svg new file mode 100644 index 00000000..e7676004 --- /dev/null +++ b/packages/web/public/devices/heltec-wireless-tracker-V1-0.svg @@ -0,0 +1,2031 @@ + + + diff --git a/packages/web/public/devices/heltec-wireless-tracker.svg b/packages/web/public/devices/heltec-wireless-tracker.svg new file mode 100644 index 00000000..e7676004 --- /dev/null +++ b/packages/web/public/devices/heltec-wireless-tracker.svg @@ -0,0 +1,2031 @@ + + + diff --git a/packages/web/public/devices/heltec-wsl-v3.svg b/packages/web/public/devices/heltec-wsl-v3.svg new file mode 100644 index 00000000..4dccfd5d --- /dev/null +++ b/packages/web/public/devices/heltec-wsl-v3.svg @@ -0,0 +1,1068 @@ + + + diff --git a/packages/web/public/devices/nano-g2-ultra.svg b/packages/web/public/devices/nano-g2-ultra.svg new file mode 100644 index 00000000..e90c2359 --- /dev/null +++ b/packages/web/public/devices/nano-g2-ultra.svg @@ -0,0 +1,216 @@ + + + diff --git a/packages/web/public/devices/pico.svg b/packages/web/public/devices/pico.svg new file mode 100644 index 00000000..15d60bf5 --- /dev/null +++ b/packages/web/public/devices/pico.svg @@ -0,0 +1,3510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web/public/devices/promicro.svg b/packages/web/public/devices/promicro.svg new file mode 100644 index 00000000..4ca7d076 --- /dev/null +++ b/packages/web/public/devices/promicro.svg @@ -0,0 +1,2619 @@ + + + diff --git a/packages/web/public/devices/rak-wismeshtap.svg b/packages/web/public/devices/rak-wismeshtap.svg new file mode 100644 index 00000000..5801916e --- /dev/null +++ b/packages/web/public/devices/rak-wismeshtap.svg @@ -0,0 +1,264 @@ + + + diff --git a/packages/web/public/devices/rak11310.svg b/packages/web/public/devices/rak11310.svg new file mode 100644 index 00000000..c5f90bab --- /dev/null +++ b/packages/web/public/devices/rak11310.svg @@ -0,0 +1,2794 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web/public/devices/rak2560.svg b/packages/web/public/devices/rak2560.svg new file mode 100644 index 00000000..137df86a --- /dev/null +++ b/packages/web/public/devices/rak2560.svg @@ -0,0 +1,379 @@ + + + diff --git a/packages/web/public/devices/rak4631.svg b/packages/web/public/devices/rak4631.svg new file mode 100644 index 00000000..d107dd67 --- /dev/null +++ b/packages/web/public/devices/rak4631.svg @@ -0,0 +1,4244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web/public/devices/rak4631_case.svg b/packages/web/public/devices/rak4631_case.svg new file mode 100644 index 00000000..bcc88e29 --- /dev/null +++ b/packages/web/public/devices/rak4631_case.svg @@ -0,0 +1,322 @@ + + + diff --git a/packages/web/public/devices/rpipicow.svg b/packages/web/public/devices/rpipicow.svg new file mode 100644 index 00000000..49950a95 --- /dev/null +++ b/packages/web/public/devices/rpipicow.svg @@ -0,0 +1,2305 @@ + + + diff --git a/packages/web/public/devices/seeed-sensecap-indicator.svg b/packages/web/public/devices/seeed-sensecap-indicator.svg new file mode 100644 index 00000000..2ec73bb4 --- /dev/null +++ b/packages/web/public/devices/seeed-sensecap-indicator.svg @@ -0,0 +1,546 @@ + + + diff --git a/packages/web/public/devices/seeed-xiao-s3.svg b/packages/web/public/devices/seeed-xiao-s3.svg new file mode 100644 index 00000000..a7f8046d --- /dev/null +++ b/packages/web/public/devices/seeed-xiao-s3.svg @@ -0,0 +1,1002 @@ + + + diff --git a/packages/web/public/devices/station-g2.svg b/packages/web/public/devices/station-g2.svg new file mode 100644 index 00000000..efc702d7 --- /dev/null +++ b/packages/web/public/devices/station-g2.svg @@ -0,0 +1,512 @@ + + + diff --git a/packages/web/public/devices/t-deck.svg b/packages/web/public/devices/t-deck.svg new file mode 100644 index 00000000..7b025e76 --- /dev/null +++ b/packages/web/public/devices/t-deck.svg @@ -0,0 +1,697 @@ + + QWERTYIUPOASDFGHKJLaltZXCVBMN + diff --git a/packages/web/public/devices/t-echo.svg b/packages/web/public/devices/t-echo.svg new file mode 100644 index 00000000..fc21b063 --- /dev/null +++ b/packages/web/public/devices/t-echo.svg @@ -0,0 +1,195 @@ + + + diff --git a/packages/web/public/devices/t-watch-s3.svg b/packages/web/public/devices/t-watch-s3.svg new file mode 100644 index 00000000..9bd241a2 --- /dev/null +++ b/packages/web/public/devices/t-watch-s3.svg @@ -0,0 +1,126 @@ + + + diff --git a/packages/web/public/devices/tbeam-s3-core.svg b/packages/web/public/devices/tbeam-s3-core.svg new file mode 100644 index 00000000..522c7c8b --- /dev/null +++ b/packages/web/public/devices/tbeam-s3-core.svg @@ -0,0 +1,2514 @@ + + + diff --git a/packages/web/public/devices/tbeam.svg b/packages/web/public/devices/tbeam.svg new file mode 100644 index 00000000..cc6797f8 --- /dev/null +++ b/packages/web/public/devices/tbeam.svg @@ -0,0 +1,3447 @@ + + + diff --git a/packages/web/public/devices/tlora-c6.svg b/packages/web/public/devices/tlora-c6.svg new file mode 100644 index 00000000..bf906e1e --- /dev/null +++ b/packages/web/public/devices/tlora-c6.svg @@ -0,0 +1,493 @@ + + + diff --git a/packages/web/public/devices/tlora-t3s3-epaper.svg b/packages/web/public/devices/tlora-t3s3-epaper.svg new file mode 100644 index 00000000..8df65f47 --- /dev/null +++ b/packages/web/public/devices/tlora-t3s3-epaper.svg @@ -0,0 +1,294 @@ + + + diff --git a/packages/web/public/devices/tlora-t3s3-v1.svg b/packages/web/public/devices/tlora-t3s3-v1.svg new file mode 100644 index 00000000..d5356e66 --- /dev/null +++ b/packages/web/public/devices/tlora-t3s3-v1.svg @@ -0,0 +1,1311 @@ + + + diff --git a/packages/web/public/devices/tlora-v2-1-1_6.svg b/packages/web/public/devices/tlora-v2-1-1_6.svg new file mode 100644 index 00000000..7a4c8b4f --- /dev/null +++ b/packages/web/public/devices/tlora-v2-1-1_6.svg @@ -0,0 +1,1237 @@ + + + diff --git a/packages/web/public/devices/tlora-v2-1-1_8.svg b/packages/web/public/devices/tlora-v2-1-1_8.svg new file mode 100644 index 00000000..7a4c8b4f --- /dev/null +++ b/packages/web/public/devices/tlora-v2-1-1_8.svg @@ -0,0 +1,1237 @@ + + + diff --git a/packages/web/public/devices/tracker-t1000-e.svg b/packages/web/public/devices/tracker-t1000-e.svg new file mode 100644 index 00000000..9ed42cf2 --- /dev/null +++ b/packages/web/public/devices/tracker-t1000-e.svg @@ -0,0 +1,441 @@ + + + diff --git a/packages/web/public/devices/unknown.svg b/packages/web/public/devices/unknown.svg new file mode 100644 index 00000000..cbafa4ef --- /dev/null +++ b/packages/web/public/devices/unknown.svg @@ -0,0 +1,242 @@ + + + + diff --git a/packages/web/public/devices/wio-tracker-wm1110.svg b/packages/web/public/devices/wio-tracker-wm1110.svg new file mode 100644 index 00000000..2f5b6903 --- /dev/null +++ b/packages/web/public/devices/wio-tracker-wm1110.svg @@ -0,0 +1,3445 @@ + + LoRaWI FILEDRESETGNSSBLE + diff --git a/packages/web/public/devices/wm1110_dev_kit.svg b/packages/web/public/devices/wm1110_dev_kit.svg new file mode 100644 index 00000000..48da0230 --- /dev/null +++ b/packages/web/public/devices/wm1110_dev_kit.svg @@ -0,0 +1,4908 @@ + + + diff --git a/public/favicon.ico b/packages/web/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to packages/web/public/favicon.ico diff --git a/packages/web/public/i18n/locales/bg-BG/channels.json b/packages/web/public/i18n/locales/bg-BG/channels.json new file mode 100644 index 00000000..55eb07d9 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Канали", + "channelName": "Канал: {{channelName}}", + "broadcastLabel": "Primary", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Настройки на канала", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Роля", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Генериране" + }, + "name": { + "label": "Име", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Местоположение", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Да не се споделя местоположението", + "precise": "Точно местоположение", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/commandPalette.json b/packages/web/public/i18n/locales/bg-BG/commandPalette.json new file mode 100644 index 00000000..a37dbd60 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "Няма намерени резултати.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Съобщения", + "map": "Карта", + "config": "Конфигурация", + "channels": "Канали", + "nodes": "Възли" + } + }, + "manage": { + "label": "Управление", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Свързване на нов възел" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR код", + "qrGenerator": "Генератор", + "qrImport": "Импортиране", + "scheduleShutdown": "Планирано изключване", + "scheduleReboot": "Планирано рестартиране", + "rebootToOtaMode": "Рестартиране в режим OTA", + "resetNodeDb": "Нулиране на базата данни с възли", + "factoryResetDevice": "Фабрично нулиране на устройството", + "factoryResetConfig": "Фабрично нулиране на конфигурацията" + } + }, + "debug": { + "label": "Отстраняване на грешки", + "command": { + "reconfigure": "Преконфигуриране", + "clearAllStoredMessages": "Изчистване на всички съхранени съобщения" + } + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/common.json b/packages/web/public/i18n/locales/bg-BG/common.json new file mode 100644 index 00000000..da05c539 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Приложи", + "backupKey": "Backup Key", + "cancel": "Отказ", + "clearMessages": "Изчистване на съобщенията", + "close": "Затвори", + "confirm": "Потвърждаване", + "delete": "Изтриване", + "dismiss": "Отхвърляне", + "download": "Изтегляне", + "export": "Експортиране", + "generate": "Генериране", + "regenerate": "Регенериране", + "import": "Импортиране", + "message": "Съобщение", + "now": "Сега", + "ok": "Добре", + "print": "Отпечатване", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Изтрий", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Нулиране", + "save": "Запис", + "scanQr": "Сканиране на QR кода", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web клиент" + }, + "loading": "Зареждане...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Метър", + "plural": "Метри", + "suffix": "m" + }, + "minute": { + "one": "Минута", + "plural": "Минути" + }, + "hour": { + "one": "Час", + "plural": "Часа" + }, + "millisecond": { + "one": "Милисекунда", + "plural": "Милисекунди", + "suffix": "ms" + }, + "second": { + "one": "Секунда", + "plural": "Секунди" + }, + "day": { + "one": "Ден", + "plural": "Дни" + }, + "month": { + "one": "Месец", + "plural": "Месеца" + }, + "year": { + "one": "Година", + "plural": "Години" + }, + "snr": "SNR", + "volt": { + "one": "Волт", + "plural": "Волта", + "suffix": "V" + }, + "record": { + "one": "Записи", + "plural": "Записи" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Неизвестно", + "shortName": "НЕИЗВ.", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "НЕЗАДАДЕН", + "fallbackName": "Meshtastic {{last4}}", + "node": "Възел", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Невалиден формат, очаква се IPv4 адрес.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Невалиден тип, очаква се число." + }, + "pskLength": { + "0bit": "Ключът трябва да е празен.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "Това поле е задължително.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Ключът е задължителен." + } + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/dashboard.json b/packages/web/public/i18n/locales/bg-BG/dashboard.json new file mode 100644 index 00000000..4f1c9d39 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Свързани устройства", + "description": "Управлявайте свързаните си устройства Meshtastic.", + "connectionType_ble": "BLE", + "connectionType_serial": "Серийна", + "connectionType_network": "Мрежа", + "noDevicesTitle": "Няма свързани устройства", + "noDevicesDescription": "Свържете ново устройство, за да започнете.", + "button_newConnection": "Нова връзка" + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/deviceConfig.json b/packages/web/public/i18n/locales/bg-BG/deviceConfig.json new file mode 100644 index 00000000..f7e707a7 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Конфигурация", + "tabBluetooth": "Bluetooth", + "tabDevice": "Устройство", + "tabDisplay": "Дисплей", + "tabLora": "LoRa", + "tabNetwork": "Мрежа", + "tabPosition": "Позиция", + "tabPower": "Захранване", + "tabSecurity": "Сигурност" + }, + "sidebar": { + "label": "Модули" + }, + "device": { + "title": "Настройки на устройството", + "description": "Настройки за устройството", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX часова зона" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Роля" + } + }, + "bluetooth": { + "title": "Настройки за Bluetooth", + "description": "Настройки за Bluetooth модула", + "note": "Забележка: Някои устройства (ESP32) не могат да използват едновременно Bluetooth и WiFi.", + "enabled": { + "description": "Активиране или дезактивиране на Bluetooth", + "label": "Активиран" + }, + "pairingMode": { + "description": "Поведение при избор на ПИН.", + "label": "Режим на сдвояване" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "ПИН" + } + }, + "display": { + "description": "Настройки за дисплея на устройството", + "title": "Настройки на дисплея", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "Колко бързо да се превключва между прозорците", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Показване на метрични или имперски мерни единици", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Използване на 12-часов формат на часовника", + "label": "12-часов часовник" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Настройки на Mesh", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Широчина на канала в MHz", + "label": "Широчина на честотната лента" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Отместване на честотата" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Честотен слот" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Ignore MQTT" + }, + "modemPreset": { + "description": "Използване на предварително настроен модем", + "label": "Предварително настроен модем" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "OK to MQTT" + }, + "overrideDutyCycle": { + "description": "Override Duty Cycle", + "label": "Override Duty Cycle" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Регион" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Активиране/дезактивиране на предаването (TX) от LoRa радиото", + "label": "Предаването е активирано" + }, + "transmitPower": { + "description": "Максимална мощност на предаване", + "label": "Мощност на предаване" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Използване на предварително зададени настройки" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Настройки на Mesh" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Настройки на радиото", + "description": "Настройки за LoRa радиото" + } + }, + "network": { + "title": "Конфигурация на WiFi", + "description": "Конфигурация на WiFi радиото", + "note": "Забележка: Някои устройства (ESP32) не могат да използват едновременно Bluetooth и WiFi.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS сървър", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Активиране или дезактивиране на Ethernet порта", + "label": "Активиран" + }, + "gateway": { + "description": "Шлюз по подразбиране", + "label": "Шлюз" + }, + "ip": { + "description": "IP адрес", + "label": "IP" + }, + "psk": { + "description": "Парола за мрежата", + "label": "PSK" + }, + "ssid": { + "description": "Име на мрежата", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Subnet" + }, + "wifiEnabled": { + "description": "Активиране или дезактивиране на WiFi радиото", + "label": "Активиран" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP сървър" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Конфигурация на Ethernet " + }, + "ipConfigSettings": { + "description": "Конфигуриране на IP", + "label": "Конфигурация на IP" + }, + "ntpConfigSettings": { + "description": "Конфигуриране на NTP", + "label": "Конфигурация на NTP" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "Конфигурация на UDP" + } + }, + "position": { + "title": "Настройки на позицията", + "description": "Настройки за модула за позиция", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Фиксирана позиция" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "Режим на GPS" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "Интервал на актуализиране на GPS" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Интервали" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Надморска височина", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Времево клеймо", + "unset": "Отказ", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Скорост на превозното средство" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Активиране на енергоспестяващ режим" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Конфигурация на захранването" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Насртойки на сигурността", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Частен ключ" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Публичен ключ" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Настройки за Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/dialog.json b/packages/web/public/i18n/locales/bg-BG/dialog.json new file mode 100644 index 00000000..1d9ea3f0 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "Това действие ще изчисти цялата история на съобщенията. Това не може да бъде отменено. Сигурни ли сте, че искате да продължите?", + "title": "Изчистване на всички съобщения" + }, + "deviceName": { + "description": "Устройството ще се рестартира, след като конфигурацията бъде запазена.", + "longName": "Дълго име", + "shortName": "Кратко име", + "title": "Промяна на името на устройството" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Невалиден Meshtastic URL" + }, + "channelPrefix": "Канал: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Канали:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Надморска височина: ", + "coordinates": "Координати:", + "title": "Местоположение: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Регенериране" + }, + "newDeviceDialog": { + "title": "Свързване на ново устройство", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Serial", + "useHttps": "Използване на HTTPS", + "connecting": "Свързване...", + "connect": "Свързване", + "connectionFailedAlert": { + "title": "Връзката е неуспешна", + "descriptionPrefix": "Не може да се свърже с устройството.", + "httpsHint": "Ако използвате HTTPS, може да се наложи първо да приемете самоподписан сертификат. ", + "openLinkPrefix": "Моля, отворете", + "openLinkSuffix": "в нов раздел", + "acceptTlsWarningSuffix": "", + "learnMoreLink": "Научете повече" + }, + "httpConnection": { + "label": "IP адрес/Име на хост", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "Все още няма сдвоени устройства.", + "newDeviceButton": "Ново устройство", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "Все още няма сдвоени устройства.", + "newDeviceButton": "Ново устройство" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Съобщение", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Ниво на батерията", + "channelUtilization": "Използване на канала", + "details": "Подробности:", + "deviceMetrics": "Device Metrics:", + "hardware": "Хардуер: ", + "lastHeard": "Последно чут: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Номер на възела: ", + "position": "Позиция:", + "role": "Роля: ", + "uptime": "Време на работа: ", + "voltage": "Напрежение", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "Ако загубите ключовете си, ще трябва да нулирате устройството си.", + "secureBackup": "Важно е да направите резервно копие на публичните и частните си ключове и да съхранявате резервното си копие сигурно!", + "footer": "=== КРАЙ НА КЛЮЧОВЕТЕ ===", + "header": "=== MESHTASTIC КЛЮЧОВЕ ЗА {{longName}} ({{shortName}}) ===", + "privateKey": "Частен ключ:", + "publicKey": "Публичен ключ:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Сигурни ли сте, че искате да регенерирате двойката ключове?", + "title": "Регенериране на двойката ключове" + }, + "qr": { + "addChannels": "Добавяне на канали", + "replaceChannels": "Замяна на канали", + "description": "Текущата конфигурация на LoRa също ще бъде споделена.", + "sharableUrl": "Sharable URL", + "title": "Генериране на QR код" + }, + "rebootOta": { + "title": "Планирано рестартиране", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Въведете забавяне (сек)", + "scheduled": "Насрочено е рестартиране" + }, + "reboot": { + "title": "Планирано рестартиране", + "description": "Рестартиране на свързания възел след x минути." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Сигурни ли сте, че искате да премахнете този възел?", + "title": "Премахване на възела?" + }, + "shutdown": { + "title": "Планирано изключване", + "description": "Изключване на свързания възел след x минути." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Да, знам какво правя", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Сигурни ли сте?" + }, + "managedMode": { + "confirmUnderstanding": "Да, знам какво правя", + "title": "Сигурни ли сте?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/messages.json b/packages/web/public/i18n/locales/bg-BG/messages.json new file mode 100644 index 00000000..edad2226 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Съобщения: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Изберете чат", + "text": "Все още няма съобщения." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Въведете Вашето съобщение тук...", + "sendButton": "Изпрати" + }, + "actionsMenu": { + "addReactionLabel": "Добавяне на реакция", + "replyLabel": "Отговор" + }, + "deliveryStatus": { + "delivered": { + "label": "Съобщението е доставено", + "displayText": "Съобщението е доставено" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Статусът на съобщението е неизвестен", + "displayText": "Неизвестно състояние" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/moduleConfig.json b/packages/web/public/i18n/locales/bg-BG/moduleConfig.json new file mode 100644 index 00000000..1b3eb924 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Ambient Lighting", + "tabAudio": "Аудио", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Detection Sensor", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Neighbor Info", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Range Test", + "tabSerial": "Serial", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Телеметрия" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Current", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Червен", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Зелен", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Син", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Настройки на аудиото", + "description": "Настройки за аудио модула", + "codec2Enabled": { + "label": "Codec 2 е активиран", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Модулът е активиран", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Ротационен енкодер #1 е активиран", + "description": "Активиране на ротационния енкодер" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Изберете от: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Активиран", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Приятелско име", + "description": "Използва се за форматиране на съобщението, изпратено до mesh, максимум 20 знака" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Модулът е активиран", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Предупредително съобщение", + "description": "Предупредително съобщение" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "Настройки на MQTT", + "description": "Настройки за MQTT модула", + "enabled": { + "label": "Активиран", + "description": "Активиране или дезактивиране на MQTT" + }, + "address": { + "label": "Адрес на MQTT сървъра", + "description": "Адрес на MQTT сървъра, който да се използва за сървъри по подразбиране/персонализирани сървъри" + }, + "username": { + "label": "Потребителско име за MQTT", + "description": "Потребителско име за MQTT, което да се използва за сървъри по подразбиране/персонализирани сървъри" + }, + "password": { + "label": "Парола за MQTT", + "description": "Парола за MQTT, която да се използва за сървъри по подразбиране/персонализирани сървъри" + }, + "encryptionEnabled": { + "label": "Криптирането е активирано", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON е активиран", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS е активиран", + "description": "Активиране или дезактивиране на TLS" + }, + "root": { + "label": "Root topic", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Приблизително местоположение", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Активиран", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Настройки за модула Paxcounter", + "enabled": { + "label": "Модулът е активиран", + "description": "Активиране на Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "Само ESP32" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Модулът е активиран", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Timeout", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Режим", + "description": "Избор на режим" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Модулът е активиран", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Брой записи", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Настройки на телеметрията", + "description": "Настройки за модула за телеметрия", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Device metrics update interval (seconds)" + }, + "environmentUpdateInterval": { + "label": "Environment metrics update interval (seconds)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Модулът е активиран", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Показване на Фаренхайт", + "description": "Показване на температурата във Фаренхайт" + }, + "airQualityEnabled": { + "label": "Качество на въздуха е активирано", + "description": "Активиране на телеметрията за качеството на въздуха" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/nodes.json b/packages/web/public/i18n/locales/bg-BG/nodes.json new file mode 100644 index 00000000..760c708d --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Публичният ключ е активиран" + }, + "noPublicKey": { + "label": "Няма публичен ключ" + }, + "directMessage": { + "label": "Директно съобщение {{shortName}}" + }, + "favorite": { + "label": "Любим", + "tooltip": "Добавяне или премахване на този възел от любимите ви" + }, + "notFavorite": { + "label": "Не е любим" + }, + "error": { + "label": "Грешка", + "text": "Възникна грешка при извличането на подробности за възела. Моля, опитайте отново по-късно." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Дълго име", + "connection": "Connection", + "lastHeard": "Последно чут", + "encryption": "Криптиране", + "model": "Модел", + "macAddress": "MAC адрес" + }, + "connectionStatus": { + "direct": "Direct", + "away": "away", + "unknown": "-", + "viaMqtt": ", чрез MQTT" + }, + "lastHeardStatus": { + "never": "Никога" + } + }, + "actions": { + "added": "Добавен", + "removed": "Премахнат", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/bg-BG/ui.json b/packages/web/public/i18n/locales/bg-BG/ui.json new file mode 100644 index 00000000..e635ba10 --- /dev/null +++ b/packages/web/public/i18n/locales/bg-BG/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Навигация", + "messages": "Съобщения", + "map": "Карта", + "config": "Конфигурация", + "radioConfig": "Конфигурация на радиото", + "moduleConfig": "Конфигурация на модула", + "channels": "Канали", + "nodes": "Възли" + }, + "app": { + "title": "Meshtastic", + "logo": "Лого на Meshtastic" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Отваряне на страничната лента", + "close": "Затваряне на страничната лента" + } + }, + "deviceInfo": { + "volts": "{{voltage}} волта", + "firmware": { + "title": "Фърмуер", + "version": "v{{version}}", + "buildDate": "Дата на компилация: {{date}}" + }, + "deviceName": { + "title": "Име на устройството", + "changeName": "Промяна на името на устройството", + "placeholder": "Въвеждане на име на устройството" + }, + "editDeviceName": "Редактиране на името на устройство" + } + }, + "batteryStatus": { + "charging": "{{level}}% зареждане", + "pluggedIn": "Включен в ел. мрежа", + "title": "Батерия" + }, + "search": { + "nodes": "Търсене на възли...", + "channels": "Търсене на канали...", + "commandPalette": "Търсене на команди..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Запазен канал: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Чатът използва PKI криптиране." + }, + "pskEncryption": { + "title": "Чатът използва PSK криптиране." + } + }, + "configSaveError": { + "title": "Грешка при запазване на конфигурацията", + "description": "Възникна грешка при запазване на конфигурацията." + }, + "validationError": { + "title": "Съществуват грешки в конфигурацията", + "description": "Моля, коригирайте грешките в конфигурацията, преди да я запазите." + }, + "saveSuccess": { + "title": "Запазване на конфигурацията", + "description": "Промяната в конфигурацията {{case}} е запазена." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Копирано!" + }, + "copyToClipboard": { + "label": "Копиране в клипборда" + }, + "hidePassword": { + "label": "Скриване на паролата" + }, + "showPassword": { + "label": "Показване на паролата" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Изчакване", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Хардуер" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Роля" + }, + "filter": { + "label": "Филтър" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Нулиране на филтрите" + }, + "nodeName": { + "label": "Име/номер на възел", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Ниво на батерията (%)", + "labelText": "Ниво на батерията (%): {{value}}" + }, + "batteryVoltage": { + "label": "Напрежение на батерията (V)", + "title": "Напрежение" + }, + "channelUtilization": { + "label": "Използване на канала (%)" + }, + "hops": { + "direct": "Direct", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Последно чут", + "labelText": "Last heard: {{value}}", + "nowLabel": "Сега" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Любими" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Свързан чрез MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Език", + "changeLanguage": "Промяна на езика" + }, + "theme": { + "dark": "Тъмна", + "light": "Светла", + "system": "Автоматично", + "changeTheme": "Промяна на цветовата схема" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Моля, включете следната информация в доклада си:", + "reportSteps": { + "step1": "Какво правехте, когато възникна грешката", + "step2": "Какво очаквахте да се случи", + "step3": "Какво всъщност се случи", + "step4": "Всяка друга подходяща информация" + }, + "reportLink": "Можете да съобщите за проблема в нашия <0>GitHub", + "dashboardLink": "Връщане към <0>таблото", + "detailsSummary": "Подробности за грешката", + "errorMessageLabel": "Съобщение за грешка:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Задвижвано от <0>▲ Vercel | Meshtastic® е регистрирана търговска марка на Meshtastic LLC. | <1>Правна информация", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/channels.json b/packages/web/public/i18n/locales/cs-CZ/channels.json new file mode 100644 index 00000000..33ae9f54 --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Kanály", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Primární", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Nastavení kanálu", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Role", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "Jméno", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/commandPalette.json b/packages/web/public/i18n/locales/cs-CZ/commandPalette.json new file mode 100644 index 00000000..df166149 --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Zprávy", + "map": "Mapa", + "config": "Config", + "channels": "Kanály", + "nodes": "Uzly" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "Import", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/common.json b/packages/web/public/i18n/locales/cs-CZ/common.json new file mode 100644 index 00000000..0d4f47ca --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Použít", + "backupKey": "Backup Key", + "cancel": "Zrušit", + "clearMessages": "Clear Messages", + "close": "Zavřít", + "confirm": "Confirm", + "delete": "Smazat", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "Import", + "message": "Zpráva", + "now": "Now", + "ok": "OK", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Odstranit", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Reset", + "save": "Uložit", + "scanQr": "Naskenovat QR kód", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/dashboard.json b/packages/web/public/i18n/locales/cs-CZ/dashboard.json new file mode 100644 index 00000000..f73dc07c --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Sériová komunikace", + "connectionType_network": "Síť", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/deviceConfig.json b/packages/web/public/i18n/locales/cs-CZ/deviceConfig.json new file mode 100644 index 00000000..6e5cb4e0 --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Zařízení", + "tabDisplay": "Obrazovka", + "tabLora": "LoRa", + "tabNetwork": "Síť", + "tabPosition": "Pozice", + "tabPower": "Napájení", + "tabSecurity": "Zabezpečení" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Dvojité klepnutí jako stisk tlačítka" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX časové pásmo" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Režim opětovného vysílání" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Role" + } + }, + "bluetooth": { + "title": "Nastavení Bluetooth", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Povoleno" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Režim párování" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Šířka pásma" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Ignorovat MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Předvolba modemu" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "OK do MQTT" + }, + "overrideDutyCycle": { + "description": "Přepsat střídu", + "label": "Přepsat střídu" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Region" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Povoleno" + }, + "gateway": { + "description": "Default Gateway", + "label": "Gateway/Brána" + }, + "ip": { + "description": "IP Address", + "label": "IP adresa" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Podsíť" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Povoleno" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "UDP Konfigurace" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Časová značka", + "unset": "Zrušit nastavení", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Povolit úsporný režim" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Nastavení napájení" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Soukromý klíč" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Veřejný klíč" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/dialog.json b/packages/web/public/i18n/locales/cs-CZ/dialog.json new file mode 100644 index 00000000..dbe2aab6 --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Sériová komunikace", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Connect", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Zpráva", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Napětí", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generate QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Jste si jistý?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "Jste si jistý?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/messages.json b/packages/web/public/i18n/locales/cs-CZ/messages.json new file mode 100644 index 00000000..e4ddb17d --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Odeslat" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Reply" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/moduleConfig.json b/packages/web/public/i18n/locales/cs-CZ/moduleConfig.json new file mode 100644 index 00000000..69d76cfe --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Ambientní osvětlení", + "tabAudio": "Zvuk", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Detekční senzor", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Informace o sousedech", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Zkouška dosahu", + "tabSerial": "Sériová komunikace", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Telemetrie" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Proud", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Červená", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Zelená", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Modrá", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Povoleno", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Povoleno", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS povoleno", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Kořenové téma", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Povoleno", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Vypršel čas spojení", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Počet záznamů", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Interval aktualizace měření spotřeby (v sekundách)" + }, + "environmentUpdateInterval": { + "label": "Interval aktualizace měření životního prostředí (v sekundách)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/nodes.json b/packages/web/public/i18n/locales/cs-CZ/nodes.json new file mode 100644 index 00000000..df4b4c6a --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Oblíbené", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Chyba", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "Přímý", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/cs-CZ/ui.json b/packages/web/public/i18n/locales/cs-CZ/ui.json new file mode 100644 index 00000000..550b2af2 --- /dev/null +++ b/packages/web/public/i18n/locales/cs-CZ/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Zprávy", + "map": "Mapa", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "Kanály", + "nodes": "Uzly" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Baterie" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Skrýt heslo" + }, + "showPassword": { + "label": "Zobrazit heslo" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Metriky" + }, + "role": { + "label": "Role" + }, + "filter": { + "label": "Filtr" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Napětí" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Přímý", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Naposledy slyšen", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Jazyk", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "Tmavý", + "light": "Světlý", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/de-DE/channels.json b/packages/web/public/i18n/locales/de-DE/channels.json new file mode 100644 index 00000000..7048124e --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Kanäle", + "channelName": "Kanal {{channelName}}", + "broadcastLabel": "Primär", + "channelIndex": "Kanal {{index}}" + }, + "validation": { + "pskInvalid": "Bitte geben Sie einen gültigen {{bits}} Bit PSK Schlüssel ein." + }, + "settings": { + "label": "Kanaleinstellungen", + "description": "Verschlüsselung, MQTT & sonstige Einstellungen" + }, + "role": { + "label": "Rolle", + "description": "Gerätetelemetrie wird über den PRIMÄR Kanal gesendet. Nur ein PRIMÄR Kanal ist erlaubt.", + "options": { + "primary": "PRIMÄR", + "disabled": "DEAKTIVIERT", + "secondary": "SEKUNDÄR" + } + }, + "psk": { + "label": "Vorher verteilter Schlüssel", + "description": "Unterstützte PSK-Längen: 256-Bit, 128-Bit, 8-Bit, leer (0-Bit)", + "generate": "Erzeugen" + }, + "name": { + "label": "Name", + "description": "Ein eindeutiger Name für den Kanal <12 Bytes. Leer lassen für Standard." + }, + "uplinkEnabled": { + "label": "Uplink aktiviert", + "description": "Nachrichten vom lokalen Netz über MQTT versenden" + }, + "downlinkEnabled": { + "label": "Downlink aktiviert", + "description": "Nachrichten von MQTT im lokalen Netz versenden" + }, + "positionPrecision": { + "label": "Standort", + "description": "Die Genauigkeit des Standorts, die in diesem Kanal geteilt werden soll. Kann deaktiviert werden.", + "options": { + "none": "Standort nicht freigeben", + "precise": "Genauer Standort", + "metric_km23": "Innerhalb von 23 Kilometern", + "metric_km12": "Innerhalb von 12 Kilometern", + "metric_km5_8": "Innerhalb von 5,8 Kilometern", + "metric_km2_9": "Innerhalb von 2,9 Kilometern", + "metric_km1_5": "Innerhalb von 1,5 Kilometern", + "metric_m700": "Innerhalb von 700 Metern", + "metric_m350": "Innerhalb von 350 Metern", + "metric_m200": "Innerhalb von 200 Metern", + "metric_m90": "Innerhalb von 90 Metern", + "metric_m50": "Innerhalb von 50 Metern", + "imperial_mi15": "Innerhalb von 15 Meilen", + "imperial_mi7_3": "Innerhalb von 7,3 Meilen", + "imperial_mi3_6": "Innerhalb von 3,6 Meilen", + "imperial_mi1_8": "Innerhalb von 1,8 Meilen", + "imperial_mi0_9": "Innerhalb von 0,9 Meilen", + "imperial_mi0_5": "Innerhalb von 0,5 Meilen", + "imperial_mi0_2": "Innerhalb von 0,2 Meilen", + "imperial_ft600": "Innerhalb von 600 Fuß", + "imperial_ft300": "Innerhalb von 300 Fuß", + "imperial_ft150": "Innerhalb von 150 Fuß" + } + } +} diff --git a/packages/web/public/i18n/locales/de-DE/commandPalette.json b/packages/web/public/i18n/locales/de-DE/commandPalette.json new file mode 100644 index 00000000..63114907 --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "Keine Ergebnisse gefunden.", + "page": { + "title": "Befehlsmenü" + }, + "pinGroup": { + "label": "Befehlsgruppe anheften" + }, + "unpinGroup": { + "label": "Befehlsgruppe lösen" + }, + "goto": { + "label": "Gehe zu", + "command": { + "messages": "Nachrichten", + "map": "Karte", + "config": "Einstellungen", + "channels": "Kanäle", + "nodes": "Knoten" + } + }, + "manage": { + "label": "Verwalten", + "command": { + "switchNode": "Knoten wechseln", + "connectNewNode": "Neuen Knoten verbinden" + } + }, + "contextual": { + "label": "Kontextabhängig", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "Importieren", + "scheduleShutdown": "Herunterfahren planen", + "scheduleReboot": "Neustarten planen", + "rebootToOtaMode": "Neustart in den OTA Modus", + "resetNodeDb": "Knotendatenbank zurücksetzen", + "factoryResetDevice": "Gerät auf Werkseinstellungen zurücksetzen", + "factoryResetConfig": "Auf Werkseinstellungen zurücksetzen" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Neu einrichten", + "clearAllStoredMessages": "Alle gespeicherten Nachrichten löschen" + } + } +} diff --git a/packages/web/public/i18n/locales/de-DE/common.json b/packages/web/public/i18n/locales/de-DE/common.json new file mode 100644 index 00000000..b02e09d6 --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Anwenden", + "backupKey": "Schlüssel sichern", + "cancel": "Abbrechen", + "clearMessages": "Nachrichten löschen", + "close": "Schließen", + "confirm": "Bestätigen", + "delete": "Löschen", + "dismiss": "Tastatur ausblenden", + "download": "Herunterladen", + "export": "Exportieren", + "generate": "Erzeugen", + "regenerate": "Neu erzeugen", + "import": "Importieren", + "message": "Nachricht", + "now": "Jetzt", + "ok": "Ok", + "print": "Drucken", + "rebootOtaNow": "Neustart in den OTA Modus", + "remove": "Entfernen", + "requestNewKeys": "Neue Schlüssel anfordern", + "requestPosition": "Standort anfordern", + "reset": "Zurücksetzen", + "save": "Speichern", + "scanQr": "QR Code scannen", + "traceRoute": "Route verfolgen", + "submit": "Absenden" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web-Applikation" + }, + "loading": "Wird geladen...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Sprung", + "plural": "Sprünge" + }, + "hopsAway": { + "one": "{{count}} Sprung entfernt", + "plural": "{{count}} Sprünge entfernt", + "unknown": "Sprungweite unbekannt" + }, + "megahertz": "MHz", + "raw": "Einheitslos", + "meter": { + "one": "Meter", + "plural": "Meter", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minuten" + }, + "hour": { + "one": "Stunde", + "plural": "Stunden" + }, + "millisecond": { + "one": "Millisekunde", + "plural": "Millisekunden", + "suffix": "ms" + }, + "second": { + "one": "Sekunde", + "plural": "Sekunden" + }, + "day": { + "one": "Tag", + "plural": "Tage" + }, + "month": { + "one": "Monat", + "plural": "Monate" + }, + "year": { + "one": "Jahr", + "plural": "Jahre" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volt", + "suffix": "V" + }, + "record": { + "one": "Datensatz", + "plural": "Datensätze" + } + }, + "security": { + "0bit": "Leer", + "8bit": "8 Bit", + "128bit": "128 Bit", + "256bit": "256 Bit" + }, + "unknown": { + "longName": "Unbekannt", + "shortName": "UNB", + "notAvailable": "Keine Angaben", + "num": "???" + }, + "nodeUnknownPrefix": "!", + "unset": "NICHT GESETZT", + "fallbackName": "Meshtastic {{last4}}", + "node": "Knoten", + "formValidation": { + "unsavedChanges": "Ungespeicherte Änderungen", + "tooBig": { + "string": "Zu lang, erwarte maximal {{maximum}} Zeichen.", + "number": "Zu groß, erwartete eine Zahl kleiner oder gleich {{maximum}}.", + "bytes": "Zu groß, erwarte maximal {{params.maximum}} Bytes." + }, + "tooSmall": { + "string": "Zu kurz, erwartete mindestens {{minimum}} Zeichen.", + "number": "Zu klein, erwartete eine Zahl größer oder gleich {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Ungültiges Format, erwartete eine IPv4 Adresse.", + "key": "Ungültiges Format, erwartet einen Base64-kodierten vor verteilten Schlüssel (PSK)." + }, + "invalidType": { + "number": "Ungültiger Typ, erwartete eine Zahl." + }, + "pskLength": { + "0bit": "Der Schlüssel muss leer sein.", + "8bit": "Der administrative Schlüssel muss ein vor verteilter 8 Bit Schlüssel (PSK) sein.", + "128bit": "Der administrative Schlüssel muss ein vor verteilter 128 Bit Schlüssel (PSK) sein.", + "256bit": "Der administrative Schlüssel muss ein vor verteilter 256 Bit Schlüssel (PSK) sein." + }, + "required": { + "generic": "Dies ist ein Pflichtfeld.", + "managed": "Mindestens ein administrativer Schlüssel wird benötigt, um diesen Knoten zu verwalten", + "key": "Schlüssel erforderlich." + } + } +} diff --git a/packages/web/public/i18n/locales/de-DE/dashboard.json b/packages/web/public/i18n/locales/de-DE/dashboard.json new file mode 100644 index 00000000..5ec2de3d --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Verbundene Geräte", + "description": "Verwalten Sie Ihre verbundenen Meshtastic Geräte.", + "connectionType_ble": "BLE", + "connectionType_serial": "Seriell", + "connectionType_network": "Netzwerk", + "noDevicesTitle": "Keine Geräte verbunden", + "noDevicesDescription": "Verbinden Sie ein neues Gerät, um zu beginnen.", + "button_newConnection": "Neue Verbindung" + } +} diff --git a/packages/web/public/i18n/locales/de-DE/deviceConfig.json b/packages/web/public/i18n/locales/de-DE/deviceConfig.json new file mode 100644 index 00000000..12a24450 --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Einstellungen", + "tabBluetooth": "Bluetooth", + "tabDevice": "Gerät", + "tabDisplay": "Display", + "tabLora": "LoRa", + "tabNetwork": "Netzwerk", + "tabPosition": "Standort", + "tabPower": "Leistung", + "tabSecurity": "Sicherheit" + }, + "sidebar": { + "label": "Module" + }, + "device": { + "title": "Geräteeinstellungen", + "description": "Einstellungen für dieses Gerät", + "buttonPin": { + "description": "GPIO für Taste überschreiben", + "label": "GPIO Taste" + }, + "buzzerPin": { + "description": "GPIO für Summer überschreiben", + "label": "GPIO Summer" + }, + "disableTripleClick": { + "description": "Dreifachklicken deaktivieren", + "label": "Dreifachklicken deaktivieren" + }, + "doubleTapAsButtonPress": { + "description": "Doppeltes Tippen als Taste verwenden", + "label": "Doppelklick als Tastendruck" + }, + "ledHeartbeatDisabled": { + "description": "Die Herzschlag LED deaktivieren", + "label": "Herzschlag LED deaktivieren" + }, + "nodeInfoBroadcastInterval": { + "description": "Häufigkeit der Übertragung von Knoteninformationen", + "label": "Knoteninfo Übertragungsintervall" + }, + "posixTimezone": { + "description": "Zeichenfolge der POSIX Zeitzone für dieses Gerät", + "label": "POSIX Zeitzone" + }, + "rebroadcastMode": { + "description": "Wie Weiterleitungen behandelt werden", + "label": "Weiterleitungsmodus" + }, + "role": { + "description": "In welche Rolle das Gerät im Netz arbeitet", + "label": "Rolle" + } + }, + "bluetooth": { + "title": "Bluetooth Einstellungen", + "description": "Einstellungen für das Bluetooth Modul", + "note": "Hinweis: Einige Geräte (ESP32) können nicht gleichzeitig Bluetooth und WLAN verwenden.", + "enabled": { + "description": "Bluetooth aktivieren oder deaktivieren", + "label": "Aktiviert" + }, + "pairingMode": { + "description": "PIN Nummer Auswahlverhalten", + "label": "Kopplungsmodus" + }, + "pin": { + "description": "PIN Nummer zum Verbinden verwenden", + "label": "PIN Nummer" + } + }, + "display": { + "description": "Einstellungen für die Geräteanzeige", + "title": "Anzeigeeinstellungen", + "headingBold": { + "description": "Überschrifttext fett darstellen", + "label": "Fette Überschrift" + }, + "carouselDelay": { + "description": "Bestimmt wie schnell die Fenster durch gewechselt werden", + "label": "Karussellverzögerung" + }, + "compassNorthTop": { + "description": "Norden im Kompass immer oben anzeigen", + "label": "Kompass Norden oben" + }, + "displayMode": { + "description": "Variante des Anzeigelayout", + "label": "Anzeigemodus" + }, + "displayUnits": { + "description": "Zeige metrische oder imperiale Einheiten", + "label": "Anzeigeeinheiten" + }, + "flipScreen": { + "description": "Anzeige um 180 Grad drehen", + "label": "Anzeige drehen" + }, + "gpsDisplayUnits": { + "description": "Anzeigeformat der Koordinaten", + "label": "GPS Anzeigeformat" + }, + "oledType": { + "description": "Art des OLED Anzeige, die an dem Gerät angeschlossen ist", + "label": "OLED Typ" + }, + "screenTimeout": { + "description": "Anzeige nach dieser Zeit automatisch ausschalten", + "label": "Anzeigeabschaltung" + }, + "twelveHourClock": { + "description": "12-Stunden Format benutzen", + "label": "12-Stunden Uhr" + }, + "wakeOnTapOrMotion": { + "description": "Gerät durch Tippen oder Bewegung aufwecken", + "label": "Aufwachen durch Tippen oder Bewegung" + } + }, + "lora": { + "title": "Netzeinstellungen", + "description": "Einstellungen für das LoRa Netz", + "bandwidth": { + "description": "Kanalbandbreite in MHz", + "label": "Bandbreite" + }, + "boostedRxGain": { + "description": "Erhöhte Empfangsverstärkung", + "label": "Erhöhte Empfangsverstärkung" + }, + "codingRate": { + "description": "Kodierrate", + "label": "Fehlerkorrektur" + }, + "frequencyOffset": { + "description": "Frequenzversatz zur Kalibrierung von Oszillatorfehlern", + "label": "Frequenzversatz" + }, + "frequencySlot": { + "description": "LoRa Frequenzschlitz", + "label": "Frequenzschlitz" + }, + "hopLimit": { + "description": "Maximale Sprungweite", + "label": "Sprungweite" + }, + "ignoreMqtt": { + "description": "MQTT Nachrichten nicht über das Netz weiterleiten", + "label": "MQTT ignorieren" + }, + "modemPreset": { + "description": "Modem Voreinstellung die verwendet wird", + "label": "Modem Voreinstellungen" + }, + "okToMqtt": { + "description": "Wenn auf aktiviert, zeigt diese Einstellung an, dass der Benutzer das Weiterleiten von Nachrichten über MQTT akzeptiert. Wenn deaktiviert, werden entfernte Knoten aufgefordert, Nachrichten nicht über MQTT weiterzuleiten", + "label": "OK für MQTT" + }, + "overrideDutyCycle": { + "description": "Duty-Cycle überschreiben", + "label": "Duty-Cycle überschreiben" + }, + "overrideFrequency": { + "description": "Sendefrequenz überschreiben (MHz)", + "label": "Sendefrequenz überschreiben" + }, + "region": { + "description": "Legt die Region für Ihren Knoten fest", + "label": "Region" + }, + "spreadingFactor": { + "description": "Anzahl der Symbole zur Kodierung der Nutzdaten", + "label": "Spreizfaktor" + }, + "transmitEnabled": { + "description": "Sender (TX) des LoRa Funkgerätes aktivieren/deaktivieren", + "label": "Senden aktiviert" + }, + "transmitPower": { + "description": "Maximale Sendeleistung", + "label": "Sendeleistung" + }, + "usePreset": { + "description": "Eine der vordefinierten Modem Voreinstellungen verwenden", + "label": "Voreinstellung verwenden" + }, + "meshSettings": { + "description": "Einstellungen für das LoRa Netz", + "label": "Netzeinstellungen" + }, + "waveformSettings": { + "description": "Einstellungen für die LoRa Wellenform", + "label": "Einstellungen der Wellenform" + }, + "radioSettings": { + "label": "Radio-Einstellungen", + "description": "Einstellungen für das LoRa Funkgerät" + } + }, + "network": { + "title": "WLAN Einstellungen", + "description": "WLAN Funkeinstellungen", + "note": "Hinweis: Einige Geräte (ESP32) können nicht gleichzeitig Bluetooth und WLAN verwenden.", + "addressMode": { + "description": "Auswahl der IP Adressenzuweisung", + "label": "IP Adressenmodus" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Aktivieren oder deaktivieren sie den Ethernet Anschluss", + "label": "Aktiviert" + }, + "gateway": { + "description": "Standard Gateway", + "label": "Gateway" + }, + "ip": { + "description": "IP Adresse", + "label": "IP" + }, + "psk": { + "description": "Netzwerkpasswort", + "label": "PSK" + }, + "ssid": { + "description": "Netzwerkname", + "label": "SSID" + }, + "subnet": { + "description": "Subnetzmaske", + "label": "Subnetz" + }, + "wifiEnabled": { + "description": "Aktivieren oder deaktivieren Sie die WLAN Übertragung", + "label": "Aktiviert" + }, + "meshViaUdp": { + "label": "Netz über UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Einstellung des Ethernet Ports", + "label": "Ethernet Einstellung" + }, + "ipConfigSettings": { + "description": "Einstellung der IP Adresse", + "label": "IP Adresseinstellungen" + }, + "ntpConfigSettings": { + "description": "NTP Server Einstellungen", + "label": "NTP Einstellungen" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog Einstellung", + "label": "Rsyslog Einstellung" + }, + "udpConfigSettings": { + "description": "UDP über Netz Einstellung", + "label": "UDP Konfiguration" + } + }, + "position": { + "title": "Standorteinstellung", + "description": "Einstellungen für das Standortmodul", + "broadcastInterval": { + "description": "Wie oft Ihr Standort über das Netz gesendet wird", + "label": "Übertragungsintervall" + }, + "enablePin": { + "description": "Überschreiben des GPIO der das GPS-Modul aktiviert", + "label": "GPIO GPS aktivieren" + }, + "fixedPosition": { + "description": "GPS Standort nicht senden, sondern manuell angegeben", + "label": "Fester Standort" + }, + "gpsMode": { + "description": "Einstellung, ob GPS des Geräts aktiviert, deaktiviert oder nicht vorhanden ist", + "label": "GPS Modus" + }, + "gpsUpdateInterval": { + "description": "Wie oft ein GPS Standort ermittelt werden soll", + "label": "GPS Aktualisierungsintervall" + }, + "positionFlags": { + "description": "Optionalen, die bei der Zusammenstellung von Standortnachrichten enthalten sein sollen. Je mehr Optionen ausgewählt werden, desto größer wird die Nachricht und die längere Übertragungszeit erhöht das Risiko für einen Nachrichtenverlust.", + "label": "Standort Optionen" + }, + "receivePin": { + "description": "GPIO Pin für serielles Empfangen (RX) des GPS-Moduls überschreiben", + "label": "GPIO Empfangen" + }, + "smartPositionEnabled": { + "description": "Standort nur verschicken, wenn eine sinnvolle Standortänderung stattgefunden hat", + "label": "Intelligenten Standort aktivieren" + }, + "smartPositionMinDistance": { + "description": "Mindestabstand (in Meter), die vor dem Senden einer Standortaktualisierung zurückgelegt werden muss", + "label": "Minimale Entfernung für intelligenten Standort" + }, + "smartPositionMinInterval": { + "description": "Minimales Intervall (in Sekunden), das vor dem Senden einer Standortaktualisierung vergangen sein muss", + "label": "Minimales Intervall für intelligenten Standort" + }, + "transmitPin": { + "description": "GPIO Pin für serielles Senden (TX) des GPS-Moduls überschreiben", + "label": "GPIO Senden" + }, + "intervalsSettings": { + "description": "Wie oft Standortaktualisierungen gesendet werden", + "label": "Intervalle" + }, + "flags": { + "placeholder": "Standort Optionen auswählen", + "altitude": "Höhe", + "altitudeGeoidalSeparation": "Geoidale Höhentrennung", + "altitudeMsl": "Höhe in Bezug auf Meeresspiegel", + "dop": "Dilution of Präzision (DOP) PDOP standardmäßig verwenden", + "hdopVdop": "Wenn DOP gesetzt ist, wird HDOP / VDOP anstelle von PDOP verwendet", + "numSatellites": "Anzahl Satelliten", + "sequenceNumber": "Sequenznummer", + "timestamp": "Zeitstempel", + "unset": "Nicht konfiguriert", + "vehicleHeading": "Fahrzeugsteuerkurs", + "vehicleSpeed": "Fahrzeuggeschwindigkeit" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Zur Optimierung der Genauigkeit bei der Akkuspannunsmessung", + "label": "ADC Multiplikationsfaktor" + }, + "ina219Address": { + "description": "Adresse des INA219 Stromsensors", + "label": "INA219 Adresse" + }, + "lightSleepDuration": { + "description": "Wie lange das Gerät im leichten Schlafmodus ist", + "label": "Dauer leichter Schlafmodus" + }, + "minimumWakeTime": { + "description": "Minimale Zeitspanne für die das Gerät aktiv bleibt, nachdem es eine Nachricht empfangen hat", + "label": "Minimale Aufwachzeit" + }, + "noConnectionBluetoothDisabled": { + "description": "Wenn das Gerät keine Bluetooth Verbindung erhält, wird der BLE Funk nach dieser Zeit deaktiviert", + "label": "Keine Verbindung, Bluetooth deaktiviert" + }, + "powerSavingEnabled": { + "description": "Auswählen, wenn aus einer Stromquelle mit niedriger Kapazität (z.B Solar) betrieben wird, um den Stromverbrauch so weit wie möglich zu minimieren.", + "label": "Energiesparmodus aktivieren" + }, + "shutdownOnBatteryDelay": { + "description": "Verzögerung bis zum Abschalten der Knoten sich im Akkubetrieb befindet. 0 für unbegrenzt", + "label": "Verzögerung Akkuabschaltung" + }, + "superDeepSleepDuration": { + "description": "Wie lange das Gerät im supertiefen Schlafmodus ist", + "label": "Dauer Supertiefschlaf" + }, + "powerConfigSettings": { + "description": "Einstellungen für das Energiemodul", + "label": "Power Konfiguration" + }, + "sleepSettings": { + "description": "Einstellungen Ruhezustand für das Energiemodul", + "label": "Einstellung Ruhezustand" + } + }, + "security": { + "description": "Sicherheitseinstellungen", + "title": "Sicherheitseinstellungen", + "button_backupKey": "Schlüssel sichern", + "adminChannelEnabled": { + "description": "Erlaubt die Gerätesteuerung über den unsicheren, veralteten administrativen Kanal", + "label": "Veraltete Administrierung erlauben" + }, + "enableDebugLogApi": { + "description": "Ausgabe von Fehlerprotokollen in Echtzeit über die serielle Schnittstelle, Anzeige und Export von Standort reduzierten Geräteprotokollen über Bluetooth", + "label": "Debug-Protokoll API aktivieren" + }, + "managed": { + "description": "Wenn aktiviert, können die Geräteeinstellungen nur von einem entfernten Administratorknoten über administrative Nachrichten geändert werden. Aktivieren Sie diese Option nur, wenn mindestens ein geeigneter Administratorknoten eingerichtet, und desen öffentlicher Schlüssel wird in einem der obigen Felder gespeichert wurde.", + "label": "Verwaltet" + }, + "privateKey": { + "description": "Wird verwendet, um einen gemeinsamen Schlüssel mit einem entfernten Gerät zu erstellen", + "label": "Privater Schlüssel" + }, + "publicKey": { + "description": "Wird an andere Knoten im Netz gesendet, damit diese einen gemeinsamen geheimen Schlüssel berechnen können", + "label": "Öffentlicher Schlüssel" + }, + "primaryAdminKey": { + "description": "Erster öffentlicher Schlüssel, der berechtigt ist, administrative Nachrichten an diesen Knoten zu senden", + "label": "Erster Admin-Schlüssel" + }, + "secondaryAdminKey": { + "description": "Zweiter öffentlicher Schlüssel, der berechtigt ist, administrative Nachrichten an diesen Knoten zu senden", + "label": "Zweiter Admin-Schlüssel" + }, + "serialOutputEnabled": { + "description": "Serielle Konsole über die Stream-API", + "label": "Serielle Ausgabe aktiviert" + }, + "tertiaryAdminKey": { + "description": "Dritter öffentlicher Schlüssel, der berechtigt ist, administrative Nachrichten an diesen Knoten zu senden", + "label": "Dritter Admin-Schlüssel" + }, + "adminSettings": { + "description": "Administrator Einstellungen", + "label": "Administrator Einstellungen" + }, + "loggingSettings": { + "description": "Einstellungen für die Protokollierung", + "label": "Protokolleinstellungen" + } + } +} diff --git a/packages/web/public/i18n/locales/de-DE/dialog.json b/packages/web/public/i18n/locales/de-DE/dialog.json new file mode 100644 index 00000000..1c8e966d --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "Diese Aktion wird den Nachrichtenverlauf löschen. Dies kann nicht rückgängig gemacht werden. Sind Sie sicher, dass Sie fortfahren möchten?", + "title": "Alle Nachrichten löschen" + }, + "deviceName": { + "description": "Das Gerät wird neu gestartet, sobald die Einstellung gespeichert ist.", + "longName": "Langer Name", + "shortName": "Kurzname", + "title": "Gerätename ändern" + }, + "import": { + "description": "Die aktuelle LoRa Einstellung wird überschrieben.", + "error": { + "invalidUrl": "Ungültige Meshtastic URL" + }, + "channelPrefix": "Kanal: ", + "channelSetUrl": "Kanalsammlung / QR-Code URL", + "channels": "Kanäle:", + "usePreset": "Voreinstellung verwenden?", + "title": "Kanalsammlung importieren" + }, + "locationResponse": { + "altitude": "Höhe: ", + "coordinates": "Koordinaten: ", + "title": "Standort: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Vorab verteilten Schlüssel (PSK) neu erstellen?", + "description": "Sind Sie sicher, dass Sie den vorab verteilten Schlüssel neu erstellen möchten?", + "regenerate": "Neu erstellen" + }, + "newDeviceDialog": { + "title": "Neues Gerät verbinden", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Seriell", + "useHttps": "HTTPS verwenden", + "connecting": "Wird verbunden...", + "connect": "Verbindung herstellen", + "connectionFailedAlert": { + "title": "Verbindung fehlgeschlagen", + "descriptionPrefix": "Verbindung zum Gerät fehlgeschlagen. ", + "httpsHint": "Wenn Sie HTTPS verwenden, müssen Sie möglicherweise zuerst ein selbstsigniertes Zertifikat akzeptieren. ", + "openLinkPrefix": "Öffnen Sie ", + "openLinkSuffix": "in einem neuen Tab", + "acceptTlsWarningSuffix": ", akzeptieren Sie alle TLS-Warnungen, wenn Sie dazu aufgefordert werden, dann versuchen Sie es erneut", + "learnMoreLink": "Mehr erfahren" + }, + "httpConnection": { + "label": "IP Adresse/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "Noch keine Geräte gekoppelt.", + "newDeviceButton": "Neues Gerät", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "Noch keine Geräte gekoppelt.", + "newDeviceButton": "Neues Gerät" + }, + "validation": { + "requiresWebBluetooth": "Dieser Verbindungstyp erfordert <0>Bluetooth im Browser. Bitte verwenden Sie einen unterstützten Browser, wie Chrome oder Edge.", + "requiresWebSerial": "Dieser Verbindungstyp erfordert <0>Serielle Schnittstelle im Browser. Bitte verwenden Sie einen unterstützten Browser, wie Chrome oder Edge.", + "requiresSecureContext": "Diese Anwendung erfordert einen <0>sicheren Kontext. Bitte verbinden Sie sich über HTTPS oder localhost.", + "additionallyRequiresSecureContext": "Zusätzlich erfordert es einen <0>sicheren Kontext. Bitte verbinden Sie sich über HTTPS oder localhost." + } + }, + "nodeDetails": { + "message": "Nachricht", + "requestPosition": "Standort anfordern", + "traceRoute": "Route verfolgen", + "airTxUtilization": "Auslastung Sendezeit", + "allRawMetrics": "Alle Rohdaten", + "batteryLevel": "Akkustand", + "channelUtilization": "Kanalauslastung", + "details": "Details:", + "deviceMetrics": "Gerätekennzahlen:", + "hardware": "Hardware: ", + "lastHeard": "Zuletzt gehört: ", + "nodeHexPrefix": "Knoten ID: !", + "nodeNumber": "Knotennummer: ", + "position": "Standort:", + "role": "Rolle: ", + "uptime": "Laufzeit: ", + "voltage": "Spannung", + "title": "Knotendetails für {{identifier}}", + "ignoreNode": "Knoten ignorieren", + "removeNode": "Knoten entfernen", + "unignoreNode": "Knoten akzeptieren" + }, + "pkiBackup": { + "loseKeysWarning": "Wenn Sie Ihre Schlüssel verlieren, müssen Sie Ihr Gerät zurücksetzen.", + "secureBackup": "Es ist wichtig, dass Sie Ihre öffentlichen und privaten Schlüssel sichern und diese sicher speichern!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Privater Schlüssel:", + "publicKey": "Öffentlicher Schlüssel:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Schlüssel sichern" + }, + "pkiBackupReminder": { + "description": "Wir empfehlen die regelmäßige Sicherung Ihrer Schlüsseldaten. Möchten Sie jetzt sicheren?", + "title": "Erinnerungen für Sicherungen", + "remindLaterPrefix": "Erinnerung in:", + "remindNever": "Nie erinnern", + "backupNow": "Jetzt sichern" + }, + "pkiRegenerate": { + "description": "Sind Sie sicher, dass Sie Schlüsselpaar neu erstellen möchten?", + "title": "Schlüsselpaar neu erstellen" + }, + "qr": { + "addChannels": "Kanäle hinzufügen", + "replaceChannels": "Kanäle ersetzen", + "description": "Die aktuelle LoRa Einstellung wird ebenfalls geteilt.", + "sharableUrl": "Teilbare URL", + "title": "QR Code Erzeugen" + }, + "rebootOta": { + "title": "Neustart planen", + "description": "Startet den verbundenen Knoten nach einer Verzögerung in den OTA (Over-the-Air) Modus.", + "enterDelay": "Verzögerung eingeben (Sek.)", + "scheduled": "Neustart wurde geplant" + }, + "reboot": { + "title": "Neustart planen", + "description": "Startet den verbundenen Knoten nach x Minuten neu." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "Dies entfernt den Knoten vom Gerät und fordert neue Schlüssel an.", + "keyMismatchReasonSuffix": ". Dies liegt daran, dass der aktuelle öffentliche Schlüssel des entfernten Knotens nicht mit dem zuvor gespeicherten Schlüssel für diesen Knoten übereinstimmt.", + "unableToSendDmPrefix": "Ihr Knoten kann keine Direktnachricht an folgenden Knoten senden: " + }, + "acceptNewKeys": "Neue Schlüssel akzeptieren", + "title": "Schlüsselfehler - {{identifier}}" + }, + "removeNode": { + "description": "Sind Sie sicher, dass Sie diesen Knoten entfernen möchten?", + "title": "Knoten entfernen?" + }, + "shutdown": { + "title": "Herunterfahren planen", + "description": "Schaltet den verbundenen Knoten nach x Minuten aus." + }, + "traceRoute": { + "routeToDestination": "Route zum Ziel:", + "routeBack": "Route zurück:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Ja, ich weiß, was ich tue!", + "conjunction": " und der Blog-Beitrag über ", + "postamble": " und verstehen die Auswirkungen einer Veränderung der Rolle.", + "preamble": "Ich habe die", + "choosingRightDeviceRole": "Wahl der richtigen Geräterolle", + "deviceRoleDocumentation": "Dokumentation der Geräterolle", + "title": "Bist Du sicher?" + }, + "managedMode": { + "confirmUnderstanding": "Ja, ich weiß, was ich tue!", + "title": "Bist Du sicher?", + "description": "Das Aktivieren des verwalteten Modus blockiert das Schreiben der Einstellungen in das Funkgerät durch alle Anwendungen (einschließlich der Webanwendung). Einmal aktiviert, können die Einstellungen nur durch administrative Nachrichten geändert werden. Diese Einstellung ist für die Fernverwaltung von abgesetzten Knoten nicht erforderlich." + } +} diff --git a/packages/web/public/i18n/locales/de-DE/messages.json b/packages/web/public/i18n/locales/de-DE/messages.json new file mode 100644 index 00000000..02f1435c --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Nachrichten: {{chatName}}", + "placeholder": "Nachricht eingeben" + }, + "emptyState": { + "title": "Einen Chat auswählen", + "text": "Noch keine Nachrichten." + }, + "selectChatPrompt": { + "text": "Wählen Sie einen Kanal oder Knoten, um Nachrichten zu schreiben." + }, + "sendMessage": { + "placeholder": "Geben Sie hier Ihre Nachricht ein...", + "sendButton": "Senden" + }, + "actionsMenu": { + "addReactionLabel": "Reaktion hinzufügen", + "replyLabel": "Antworten" + }, + "deliveryStatus": { + "delivered": { + "label": "Nachricht zugestellt", + "displayText": "Nachricht zugestellt" + }, + "failed": { + "label": "Nachrichtenübermittlung fehlgeschlagen", + "displayText": "Zustellung fehlgeschlagen" + }, + "unknown": { + "label": "Nachrichtenstatus unbekannt", + "displayText": "Unbekannter Status" + }, + "waiting": { + "label": "Nachricht wird gesendet", + "displayText": "Warte auf Zustellung" + } + } +} diff --git a/packages/web/public/i18n/locales/de-DE/moduleConfig.json b/packages/web/public/i18n/locales/de-DE/moduleConfig.json new file mode 100644 index 00000000..6a6e759a --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Umgebungslicht", + "tabAudio": "Audio", + "tabCannedMessage": "Vordefinierte Nachrichten", + "tabDetectionSensor": "Erkennungssensor", + "tabExternalNotification": "Externe Benachrichtigung", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Nachbarinformation", + "tabPaxcounter": "Pax Zähler", + "tabRangeTest": "Reichweitentest", + "tabSerial": "Seriell", + "tabStoreAndForward": "Speichern&Weiterleiten", + "tabTelemetry": "Telemetrie" + }, + "ambientLighting": { + "title": "Einstellung Umgebungsbeleuchtung", + "description": "Einstellungen für das Modul Umgebungsbeleuchtung", + "ledState": { + "label": "LED Status", + "description": "Setzt die LED auf ein oder aus" + }, + "current": { + "label": "Stromstärke", + "description": "Legt den Strom für den LED Ausgang fest. Standard ist 10" + }, + "red": { + "label": "Rot", + "description": "Legt den roten LED Wert fest. Bereich 0-255" + }, + "green": { + "label": "Grün", + "description": "Legt den grünen LED Wert fest. Bereich 0-255" + }, + "blue": { + "label": "Blau", + "description": "Legt den blauen LED Wert fest. Bereich 0-255" + } + }, + "audio": { + "title": "Audioeinstellungen", + "description": "Einstellungen für das Audiomodul", + "codec2Enabled": { + "label": "Codec 2 aktiviert", + "description": "Codec 2 Audiokodierung aktivieren" + }, + "pttPin": { + "label": "GPIO PTT", + "description": "Für PTT verwendeter GPIO Pin" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate zur Audiokodierung" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO Pin für i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO Pin für i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO Pin für i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO Pin für i2S SCK" + } + }, + "cannedMessage": { + "title": "Einstellungen für vordefinierte Nachrichten", + "description": "Einstellungen für das Modul vordefinierte Nachrichten", + "moduleEnabled": { + "label": "Modul aktiviert", + "description": "Vordefinierte Nachrichten aktivieren" + }, + "rotary1Enabled": { + "label": "Drehgeber #1 aktiviert", + "description": "Drehgeber aktivieren" + }, + "inputbrokerPinA": { + "label": "Drehgeber Pin A", + "description": "GPIO Pin Wert (1-39) für Drehgeber Pin A" + }, + "inputbrokerPinB": { + "label": "Drehgeber Pin B", + "description": "GPIO Pin Wert (1-39) für Drehgeber Pin B" + }, + "inputbrokerPinPress": { + "label": "Drehgeber Pin Taste", + "description": "GPIO Pin Wert (1-39) für Drehgeber Pin Taste" + }, + "inputbrokerEventCw": { + "label": "Ereignis im Uhrzeigersinn", + "description": "Eingabeereignis auswählen." + }, + "inputbrokerEventCcw": { + "label": "Ereignis gegen Uhrzeigersinn", + "description": "Eingabeereignis auswählen." + }, + "inputbrokerEventPress": { + "label": "Ereignis Tastendruck", + "description": "Eingabeereignis auswählen." + }, + "updown1Enabled": { + "label": "Geber Hoch/Runter aktiviert", + "description": "Aktiviere Geber Hoch/Runter" + }, + "allowInputSource": { + "label": "Eingabequelle zulassen", + "description": "Wählen Sie aus: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Sende Glocke", + "description": "Sendet ein Klingelzeichen (Glocke) mit jeder Nachricht" + } + }, + "detectionSensor": { + "title": "Sensoreinstellungen für Erkennung", + "description": "Einstellungen für das Erkennungssensormodul", + "enabled": { + "label": "Aktiviert", + "description": "Erkennungssensormodul aktivieren oder deaktivieren" + }, + "minimumBroadcastSecs": { + "label": "Minimale Übertragungszeit alle Sekunden", + "description": "Das Intervall in Sekunden, wie oft eine Nachricht an das Netz gesendet wird, wenn eine Statusänderung erkannt wurde" + }, + "stateBroadcastSecs": { + "label": "Statusübertragung alle Sekunden", + "description": "Das Intervall in Sekunden, wie oft eine Nachricht mit dem aktuellen Status an das Netz gesendet wird, unabhängig von Änderungen" + }, + "sendBell": { + "label": "Sende Glocke", + "description": "ASCII-Glocke mit Warnmeldung senden" + }, + "name": { + "label": "Anzeigename", + "description": "Formatierte Nachricht die an das Netz gesendet wird, maximal 20 Zeichen" + }, + "monitorPin": { + "label": "GPIO Pin überwachen", + "description": "Der GPIO Pin zur Überwachung von Statusänderungen" + }, + "detectionTriggerType": { + "label": "Auslösetyp der Erkennung", + "description": "Die Art des zu verwendenden Auslöseereignisses" + }, + "usePullup": { + "label": "Pullup verwenden", + "description": "Gibt an, ob der INPUT_PULLUP Modus für GPIO Pin verwendet wird oder nicht" + } + }, + "externalNotification": { + "title": "Einstellungen für externe Benachrichtigungen", + "description": "Einstellung für das Modul externe Benachrichtigung", + "enabled": { + "label": "Modul aktiviert", + "description": "Externe Benachrichtigung aktivieren" + }, + "outputMs": { + "label": "Ausgabe MS", + "description": "Ausgabe MS" + }, + "output": { + "label": "Ausgabe", + "description": "Ausgabe" + }, + "outputVibra": { + "label": "Ausgabe Vibration", + "description": "Ausgabe Vibration" + }, + "outputBuzzer": { + "label": "Ausgabe Summer", + "description": "Ausgabe Summer" + }, + "active": { + "label": "Aktiv", + "description": "Aktiv" + }, + "alertMessage": { + "label": "Warnmeldung", + "description": "Warnmeldung" + }, + "alertMessageVibra": { + "label": "Vibration bei Warnmeldung", + "description": "Vibration bei Warnmeldung" + }, + "alertMessageBuzzer": { + "label": "Summer bei Warnmeldung", + "description": "Summer bei Warnmeldung" + }, + "alertBell": { + "label": "Warnglocke", + "description": "Soll beim Empfang eines eingehenden Klingelzeichens (Glocke) eine Warnung ausgelöst werden?" + }, + "alertBellVibra": { + "label": "Vibration bei Klingelzeichen", + "description": "Vibration bei Klingelzeichen" + }, + "alertBellBuzzer": { + "label": "Summer bei Klingelzeichen", + "description": "Summer bei Klingelzeichen" + }, + "usePwm": { + "label": "PWM verwenden", + "description": "PWM verwenden" + }, + "nagTimeout": { + "label": "Nervige Verzögerung", + "description": "Nervige Verzögerung" + }, + "useI2sAsBuzzer": { + "label": "I2S GPIO Pin als Summer verwenden", + "description": "I2S GPIO Pin als Summerausgang definieren" + } + }, + "mqtt": { + "title": "MQTT Einstellungen", + "description": "Einstellungen für das MQTT Modul", + "enabled": { + "label": "Aktiviert", + "description": "MQTT aktivieren oder deaktivieren" + }, + "address": { + "label": "MQTT Server Adresse", + "description": "MQTT Serveradresse für Standard/benutzerdefinierte Server" + }, + "username": { + "label": "MQTT Benutzername", + "description": "MQTT Benutzername für Standard/benutzerdefinierte Server" + }, + "password": { + "label": "MQTT Passwort", + "description": "MQTT Passwort für Standard/benutzerdefinierte Server" + }, + "encryptionEnabled": { + "label": "Verschlüsselung aktiviert", + "description": "MQTT-Verschlüsselung aktivieren oder deaktivieren. Hinweis: Alle Nachrichten werden unverschlüsselt an den MQTT-Broker gesendet, wenn diese Option nicht aktiviert ist. Unabhängig von der eingestellten Kanalverschlüsselung. Einschließlich der Standortdaten." + }, + "jsonEnabled": { + "label": "JSON aktiviert", + "description": "Gibt an, ob JSON Nachrichten über MQTT gesendet oder empfangen werden sollen" + }, + "tlsEnabled": { + "label": "TLS aktiviert", + "description": "TLS aktivieren oder deaktivieren" + }, + "root": { + "label": "Hauptthema", + "description": "MQTT Hauptthema für Standard/Benutzerdefinierte Server" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy aktiviert", + "description": "Verwendet die Netzwerkverbindung zum Austausch von MQTT Nachrichten mit dem Client." + }, + "mapReportingEnabled": { + "label": "Kartenberichte aktiviert", + "description": "Ihr Knoten sendet in regelmäßigen Abständen eine unverschlüsselte Nachricht mit Kartenbericht an den konfigurierten MQTT-Server. Einschließlich ID, langen und kurzen Namen, ungefährer Standort, Hardwaremodell, Geräterolle, Firmware-Version, LoRa Region, Modem-Voreinstellung und Name des Primärkanal." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Veröffentlichungsintervall Kartenbericht (s)", + "description": "Intervall in Sekunden, um Kartenberichte zu veröffentlichen" + }, + "positionPrecision": { + "label": "Ungefährer Standort", + "description": "Der geteilte Standort mit einer Genauigkeit innerhalb dieser Entfernung", + "options": { + "metric_km23": "Innerhalb von 23 km", + "metric_km12": "Innerhalb von 12 km", + "metric_km5_8": "Innerhalb von 5,8 km", + "metric_km2_9": "Innerhalb von 2,9 km", + "metric_km1_5": "Innerhalb von 1,5 km", + "metric_m700": "Innerhalb von 700 m", + "metric_m350": "Innerhalb von 350 m", + "metric_m200": "Innerhalb von 200 m", + "metric_m90": "Innerhalb von 90 m", + "metric_m50": "Innerhalb von 50 m", + "imperial_mi15": "Innerhalb von 15 Meilen", + "imperial_mi7_3": "Innerhalb von 7,3 Meilen", + "imperial_mi3_6": "Innerhalb von 3,6 Meilen", + "imperial_mi1_8": "Innerhalb von 1,8 Meilen", + "imperial_mi0_9": "Innerhalb von 0,9 Meilen", + "imperial_mi0_5": "Innerhalb von 0,5 Meilen", + "imperial_mi0_2": "Innerhalb von 0,2 Meilen", + "imperial_ft600": "Innerhalb von 600 Fuß", + "imperial_ft300": "Innerhalb von 300 Fuß", + "imperial_ft150": "Innerhalb von 150 Fuß" + } + } + } + }, + "neighborInfo": { + "title": "Einstellung Nachbarinformation", + "description": "Einstellungen für das Modul Nachbarinformation", + "enabled": { + "label": "Aktiviert", + "description": "Nachbarinformation Modul aktivieren oder deaktivieren" + }, + "updateInterval": { + "label": "Aktualisierungsintervall", + "description": "Intervall in Sekunden, wie oft die Nachbarinformation an das Netz gesendet wird" + } + }, + "paxcounter": { + "title": "Einstellung für Pax Zähler", + "description": "Einstellungen für das Modul Pax Zähler", + "enabled": { + "label": "Modul aktiviert", + "description": "Aktiviere Pax Zähler" + }, + "paxcounterUpdateInterval": { + "label": "Aktualisierungsintervall (Sekunden)", + "description": "Wie lange soll zwischen dem Senden von Pax Zählernachrichten gewartet werden" + }, + "wifiThreshold": { + "label": "WLAN RSSI Grenzwert", + "description": "Bei welchem WLAN RSSI Grenzwert sollte der Zähler erhöht werden. Standardwert -80" + }, + "bleThreshold": { + "label": "BLE RSSI Grenzwert", + "description": "Bei welchem BLE RSSI Grenzwert sollte der Zähler erhöht werden. Standardwert -80" + } + }, + "rangeTest": { + "title": "Einstellung Reichweitentest", + "description": "Einstellungen für das Modul Reichweitentest", + "enabled": { + "label": "Modul aktiviert", + "description": "Reichweitentest aktivieren" + }, + "sender": { + "label": "Nachrichtenintervall", + "description": "Wie lange soll zwischen dem Senden von Testnachrichten gewartet werden" + }, + "save": { + "label": "CSV im internen Speicher abspeichern", + "description": "Nur für ESP32" + } + }, + "serial": { + "title": "Serielle Einstellungen", + "description": "Einstellungen für das serielle Modul", + "enabled": { + "label": "Modul aktiviert", + "description": "Serielle Ausgabe aktivieren" + }, + "echo": { + "label": "Echo", + "description": "Wenn aktiviert, werden alle Nachrichten, die Sie senden, an Ihr Gerät zurückgesendet" + }, + "rxd": { + "label": "GPIO Empfangen", + "description": "Setzen Sie den GPIO Pin, den Sie eingerichtet haben, als RXD Pin." + }, + "txd": { + "label": "GPIO Senden", + "description": "Setzen Sie den GPIO Pin, den Sie eingerichtet haben, als TXD Pin." + }, + "baud": { + "label": "Baudrate", + "description": "Serielle Baudrate" + }, + "timeout": { + "label": "Zeitlimit erreicht", + "description": "Wartezeit in Sekunden bis eine Nachricht als gesendet angenommen wird" + }, + "mode": { + "label": "Betriebsmodus", + "description": "Modus auswählen" + }, + "overrideConsoleSerialPort": { + "label": "Seriellen Port der Konsole überschreiben", + "description": "Wenn Sie einen seriellen Port an die Konsole angeschlossen haben, wird diese überschrieben." + } + }, + "storeForward": { + "title": "Speichern & Weiterleiten Einstellungen", + "description": "Einstellungen für das Modul Speichern & Weiterleiten", + "enabled": { + "label": "Modul aktiviert", + "description": "Speichern & Weiterleiten aktivieren" + }, + "heartbeat": { + "label": "Herzschlag aktiviert", + "description": "Herzschlag für Speichern & Weiterleiten aktivieren" + }, + "records": { + "label": "Anzahl Einträge", + "description": "Anzahl der zu speichernden Datensätze" + }, + "historyReturnMax": { + "label": "Verlauf Rückgabewert maximal", + "description": "Maximale Anzahl an zurückzugebenden Datensätzen" + }, + "historyReturnWindow": { + "label": "Zeitraum Rückgabewert", + "description": "Maximale Anzahl an zurückzugebenden Datensätzen" + } + }, + "telemetry": { + "title": "Telemetrieeinstellungen", + "description": "Einstellungen für das Telemetriemodul", + "deviceUpdateInterval": { + "label": "Gerätekennzahlen", + "description": "Aktualisierungsintervall für Gerätekennzahlen (Sekunden)" + }, + "environmentUpdateInterval": { + "label": "Aktualisierungsintervall für Umweltdaten (Sekunden)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Modul aktiviert", + "description": "Aktiviert die Telemetrie für Umweltdaten" + }, + "environmentScreenEnabled": { + "label": "OLED Anzeige aktivieren", + "description": "Zeige das Telemetriemodul auf der OLED Anzeige" + }, + "environmentDisplayFahrenheit": { + "label": "Temperatur in Fahrenheit", + "description": "Temperatur in Fahrenheit anzeigen" + }, + "airQualityEnabled": { + "label": "Luftqualität aktiviert", + "description": "Telemetrie für Luftqualität aktivieren" + }, + "airQualityInterval": { + "label": "Aktualisierungsintervall Luftqualität", + "description": "Wie oft werden Luftqualitätsdaten über das Netz gesendet" + }, + "powerMeasurementEnabled": { + "label": "Energiemessung aktiviert", + "description": "Aktiviere die Telemetrie für die Energiemessung" + }, + "powerUpdateInterval": { + "label": "Aktualisierungsintervall Energie", + "description": "Wie oft werden Energiedaten an das Netz gesendet" + }, + "powerScreenEnabled": { + "label": "Energieanzeige aktiviert", + "description": "Aktiviere die Anzeige für Energietelemetrie" + } + } +} diff --git a/packages/web/public/i18n/locales/de-DE/nodes.json b/packages/web/public/i18n/locales/de-DE/nodes.json new file mode 100644 index 00000000..de1e038d --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Öffentlicher Schlüssel aktiviert" + }, + "noPublicKey": { + "label": "Kein öffentlicher Schlüssel" + }, + "directMessage": { + "label": "Direktnachricht {{shortName}}" + }, + "favorite": { + "label": "Favorit", + "tooltip": "Diesen Knoten zu Favoriten hinzufügen oder entfernen" + }, + "notFavorite": { + "label": "Kein Favorit" + }, + "error": { + "label": "Fehler", + "text": "Beim Abrufen der Knotendetails ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut." + }, + "status": { + "heard": "Gehört", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Höhe" + }, + "channelUtil": { + "label": "Kanal-Auslastung" + }, + "airtimeUtil": { + "label": "Sendezeit-Auslastung" + } + }, + "nodesTable": { + "headings": { + "longName": "Langer Name", + "connection": "Verbindung", + "lastHeard": "Zuletzt gehört", + "encryption": "Verschlüsselung", + "model": "Modell", + "macAddress": "MAC Adresse" + }, + "connectionStatus": { + "direct": "Direkt", + "away": "abwesend", + "unknown": "-", + "viaMqtt": ", über MQTT" + }, + "lastHeardStatus": { + "never": "Nie" + } + }, + "actions": { + "added": "Hinzugefügt", + "removed": "Entfernt", + "ignoreNode": "Knoten ignorieren", + "unignoreNode": "Knoten akzeptieren", + "requestPosition": "Standort anfordern" + } +} diff --git a/packages/web/public/i18n/locales/de-DE/ui.json b/packages/web/public/i18n/locales/de-DE/ui.json new file mode 100644 index 00000000..bc7c1853 --- /dev/null +++ b/packages/web/public/i18n/locales/de-DE/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Nachrichten", + "map": "Karte", + "config": "Einstellungen", + "radioConfig": "Funkgerätekonfiguration", + "moduleConfig": "Moduleinstellungen", + "channels": "Kanäle", + "nodes": "Knoten" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Seitenleiste öffnen", + "close": "Seitenleiste schließen" + } + }, + "deviceInfo": { + "volts": "{{voltage}} Volt", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Erstelldatum: {{date}}" + }, + "deviceName": { + "title": "Gerätename", + "changeName": "Gerätename ändern", + "placeholder": "Gerätename eingeben" + }, + "editDeviceName": "Gerätename bearbeiten" + } + }, + "batteryStatus": { + "charging": "{{level}}% Ladung", + "pluggedIn": "Wird geladen", + "title": "Batterie" + }, + "search": { + "nodes": "Knoten suchen...", + "channels": "Kanäle suchen...", + "commandPalette": "Befehle suchen..." + }, + "toast": { + "positionRequestSent": { + "title": "Standortanfrage gesendet." + }, + "requestingPosition": { + "title": "Standort wird angefordert, bitte warten..." + }, + "sendingTraceroute": { + "title": "Sende Traceroute, bitte warten..." + }, + "tracerouteSent": { + "title": "Traceroute gesendet." + }, + "savedChannel": { + "title": "Gespeicherter Kanal: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Der Chat verwendet PKI-Verschlüsselung." + }, + "pskEncryption": { + "title": "Chat verwendet PSK-Verschlüsselung." + } + }, + "configSaveError": { + "title": "Fehler beim Speichern von Einstellung", + "description": "Beim Speichern der Einstellungen ist ein Fehler aufgetreten." + }, + "validationError": { + "title": "Einstellungsfehler vorhanden", + "description": "Bitte korrigieren Sie die Einstellungsfehler vor dem Speichern." + }, + "saveSuccess": { + "title": "Einstellungen speichern", + "description": "Die Einstellungsänderung {{case}} wurde gespeichert." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} Favoriten.", + "action": { + "added": "Hinzugefügt", + "removed": "Entfernt", + "to": "bis", + "from": "von" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} Ignorierliste", + "action": { + "added": "Hinzugefügt", + "removed": "Entfernt", + "to": "bis", + "from": "von" + } + } + }, + "notifications": { + "copied": { + "label": "Kopiert!" + }, + "copyToClipboard": { + "label": "In die Zwischenablage kopieren" + }, + "hidePassword": { + "label": "Passwort verbergen" + }, + "showPassword": { + "label": "Passwort anzeigen" + }, + "deliveryStatus": { + "delivered": "Zugestellt", + "failed": "Zustellung fehlgeschlagen", + "waiting": "Warte...", + "unknown": "Unbekannt" + } + }, + "general": { + "label": "Allgemein" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Messgrößen" + }, + "role": { + "label": "Rolle" + }, + "filter": { + "label": "Filter" + }, + "advanced": { + "label": "Fortgeschritten" + }, + "clearInput": { + "label": "Eingabe löschen" + }, + "resetFilters": { + "label": "Filter zurücksetzen" + }, + "nodeName": { + "label": "Knotenname/-nummer", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Sendezeit-Auslastung (%)" + }, + "batteryLevel": { + "label": "Akkustand (%)", + "labelText": "Akkustand (%): {{value}}" + }, + "batteryVoltage": { + "label": "Batteriespannung (V)", + "title": "Spannung" + }, + "channelUtilization": { + "label": "Kanalauslastung (%)" + }, + "hops": { + "direct": "Direkt", + "label": "Anzahl Hops", + "text": "Sprungweite: {{value}}" + }, + "lastHeard": { + "label": "Zuletzt gehört", + "labelText": "Zuletzt gehört: {{value}}", + "nowLabel": "Jetzt" + }, + "snr": { + "label": "SNR (dB)" + }, + "favorites": { + "label": "Favoriten" + }, + "hide": { + "label": "Ausblenden" + }, + "showOnly": { + "label": "Zeige nur" + }, + "viaMqtt": { + "label": "Über MQTT verbunden" + }, + "hopsUnknown": { + "label": "Unbekannte Sprungweite" + }, + "showUnheard": { + "label": "Nie gehört" + }, + "language": { + "label": "Sprache", + "changeLanguage": "Sprache ändern" + }, + "theme": { + "dark": "Dunkel", + "light": "Hell", + "system": "Automatisch", + "changeTheme": "Farbschema ändern" + }, + "errorPage": { + "title": "Das ist ein wenig peinlich...", + "description1": "Es tut uns wirklich leid, aber im Webclient ist ein Fehler aufgetreten, der es zum Absturz gebracht hat.
Das soll nicht passieren, und wir arbeiten hart daran, es zu beheben.", + "description2": "Der beste Weg, um zu verhindern, dass sich dies Ihnen oder irgendjemand anderem wiederholt, besteht darin, uns über dieses Problem zu berichten.", + "reportInstructions": "Bitte fügen Sie folgende Informationen in Ihren Bericht ein:", + "reportSteps": { + "step1": "Was haben Sie getan, als der Fehler aufgetreten ist", + "step2": "Was haben Sie erwartet", + "step3": "Was tatsächlich passiert ist", + "step4": "Sonstige relevante Informationen" + }, + "reportLink": "Sie können das Problem auf unserem <0>GitHub melden", + "dashboardLink": "Zurück zum <0>Dashboard", + "detailsSummary": "Fehlerdetails", + "errorMessageLabel": "Fehlermeldungen:", + "stackTraceLabel": "Stapelabzug:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic®️ ist eine eingetragene Marke der Meshtastic LLC. | <1>Rechtliche Informationen", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/src/i18n/locales/en/channels.json b/packages/web/public/i18n/locales/en/channels.json similarity index 100% rename from src/i18n/locales/en/channels.json rename to packages/web/public/i18n/locales/en/channels.json diff --git a/src/i18n/locales/en/commandPalette.json b/packages/web/public/i18n/locales/en/commandPalette.json similarity index 97% rename from src/i18n/locales/en/commandPalette.json rename to packages/web/public/i18n/locales/en/commandPalette.json index 7b82e97b..1eed8987 100644 --- a/src/i18n/locales/en/commandPalette.json +++ b/packages/web/public/i18n/locales/en/commandPalette.json @@ -1,7 +1,7 @@ { "emptyState": "No results found.", "page": { - "title": "Command Palette" + "title": "Command Menu" }, "pinGroup": { "label": "Pin command group" diff --git a/src/i18n/locales/en/common.json b/packages/web/public/i18n/locales/en/common.json similarity index 88% rename from src/i18n/locales/en/common.json rename to packages/web/public/i18n/locales/en/common.json index efb44b14..b9ba239e 100644 --- a/src/i18n/locales/en/common.json +++ b/packages/web/public/i18n/locales/en/common.json @@ -24,7 +24,8 @@ "reset": "Reset", "save": "Save", "scanQr": "Scan QR Code", - "traceRoute": "Trace Route" + "traceRoute": "Trace Route", + "submit": "Submit" }, "app": { "title": "Meshtastic", @@ -48,17 +49,24 @@ "raw": "raw", "meter": { "one": "Meter", "plural": "Meters", "suffix": "m" }, "minute": { "one": "Minute", "plural": "Minutes" }, + "hour": { "one": "Hour", "plural": "Hours" }, "millisecond": { "one": "Millisecond", "plural": "Milliseconds", "suffix": "ms" }, "second": { "one": "Second", "plural": "Seconds" }, + "day": { "one": "Day", "plural": "Days" }, + "month": { "one": "Month", "plural": "Months" }, + "year": { "one": "Year", "plural": "Years" }, "snr": "SNR", "volt": { "one": "Volt", "plural": "Volts", "suffix": "V" }, "record": { "one": "Records", "plural": "Records" } }, "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", "256bit": "256 bit" }, "unknown": { @@ -70,7 +78,9 @@ "nodeUnknownPrefix": "!", "unset": "UNSET", "fallbackName": "Meshtastic {{last4}}", + "node": "Node", "formValidation": { + "unsavedChanges": "Unsaved changes", "tooBig": { "string": "Too long, expected less than or equal to {{maximum}} characters.", "number": "Too big, expected a number smaller than or equal to {{maximum}}.", diff --git a/src/i18n/locales/en/dashboard.json b/packages/web/public/i18n/locales/en/dashboard.json similarity index 100% rename from src/i18n/locales/en/dashboard.json rename to packages/web/public/i18n/locales/en/dashboard.json diff --git a/src/i18n/locales/en/deviceConfig.json b/packages/web/public/i18n/locales/en/deviceConfig.json similarity index 100% rename from src/i18n/locales/en/deviceConfig.json rename to packages/web/public/i18n/locales/en/deviceConfig.json diff --git a/src/i18n/locales/en/dialog.json b/packages/web/public/i18n/locales/en/dialog.json similarity index 84% rename from src/i18n/locales/en/dialog.json rename to packages/web/public/i18n/locales/en/dialog.json index 051909c5..7455a4ed 100644 --- a/src/i18n/locales/en/dialog.json +++ b/packages/web/public/i18n/locales/en/dialog.json @@ -7,7 +7,13 @@ "description": "The Device will restart once the config is saved.", "longName": "Long Name", "shortName": "Short Name", - "title": "Change Device Name" + "title": "Change Device Name", + "validation": { + "longNameMax": "Long name must not be more than 40 characters", + "shortNameMax": "Short name must not be more than 4 characters", + "longNameMin": "Long name must have at least 1 character", + "shortNameMin": "Short name must have at least 1 character" + } }, "import": { "description": "The current LoRa configuration will be overridden.", @@ -21,9 +27,10 @@ "title": "Import Channel Set" }, "locationResponse": { + "title": "Location: {{identifier}}", "altitude": "Altitude: ", "coordinates": "Coordinates: ", - "title": "Location: {{identifier}}" + "noCoordinates": "No Coordinates" }, "pkiRegenerateDialog": { "title": "Regenerate Pre-Shared Key?", @@ -61,7 +68,12 @@ }, "bluetoothConnection": { "noDevicesPaired": "No devices paired yet.", - "newDeviceButton": "New device" + "newDeviceButton": "New device", + "connectionFailed": "Connection failed", + "deviceDisconnected": "Device disconnected", + "unknownDevice": "Unknown Device", + "errorLoadingDevices": "Error loading devices", + "unknownErrorLoadingDevices": "Unknown error loading devices" }, "validation": { "requiresFeatures": "This connection type requires <0>. Please use a supported browser, like Chrome or Edge.", @@ -133,7 +145,6 @@ "unignoreNode": "Unignore node" }, "pkiBackup": { - "description": "We recommend backing up your key data regularly. Would you like to back up now?", "loseKeysWarning": "If you lose your keys, you will need to reset your device.", "secureBackup": "Its important to backup your public and private keys and store your backup securely!", "footer": "=== END OF KEYS ===", @@ -143,6 +154,13 @@ "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", "title": "Backup Keys" }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, "pkiRegenerate": { "description": "Are you sure you want to regenerate key pair?", "title": "Regenerate Key Pair" @@ -196,5 +214,10 @@ "choosingRightDeviceRole": "Choosing The Right Device Role", "deviceRoleDocumentation": "Device Role Documentation", "title": "Are you sure?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "Are you sure?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." } } diff --git a/packages/web/public/i18n/locales/en/messages.json b/packages/web/public/i18n/locales/en/messages.json new file mode 100644 index 00000000..07d60e57 --- /dev/null +++ b/packages/web/public/i18n/locales/en/messages.json @@ -0,0 +1,40 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Send" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Reply" + }, + + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/src/i18n/locales/en/moduleConfig.json b/packages/web/public/i18n/locales/en/moduleConfig.json similarity index 97% rename from src/i18n/locales/en/moduleConfig.json rename to packages/web/public/i18n/locales/en/moduleConfig.json index bca3468e..caacbe17 100644 --- a/src/i18n/locales/en/moduleConfig.json +++ b/packages/web/public/i18n/locales/en/moduleConfig.json @@ -253,12 +253,12 @@ "description": "MQTT root topic to use for default/custom servers" }, "proxyToClientEnabled": { - "label": "Proxy to Client Enabled", - "description": "Use the client's internet connection for MQTT (feature only active in mobile apps)" + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." }, "mapReportingEnabled": { "label": "Map Reporting Enabled", - "description": "Enable or disable map reporting" + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." }, "mapReportSettings": { "publishIntervalSecs": { diff --git a/src/i18n/locales/en/nodes.json b/packages/web/public/i18n/locales/en/nodes.json similarity index 70% rename from src/i18n/locales/en/nodes.json rename to packages/web/public/i18n/locales/en/nodes.json index 5202f039..b63e90c4 100644 --- a/src/i18n/locales/en/nodes.json +++ b/packages/web/public/i18n/locales/en/nodes.json @@ -10,11 +10,16 @@ "label": "Direct Message {{shortName}}" }, "favorite": { - "label": "Favorite" + "label": "Favorite", + "tooltip": "Add or remove this node from your favorites" }, "notFavorite": { "label": "Not a Favorite" }, + "error": { + "label": "Error", + "text": "An error occurred while fetching node details. Please try again later." + }, "status": { "heard": "Heard", "mqtt": "MQTT" @@ -47,5 +52,13 @@ "lastHeardStatus": { "never": "Never" } + }, + + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" } } diff --git a/src/i18n/locales/en/ui.json b/packages/web/public/i18n/locales/en/ui.json similarity index 86% rename from src/i18n/locales/en/ui.json rename to packages/web/public/i18n/locales/en/ui.json index 6f0b6aa3..daab3891 100644 --- a/src/i18n/locales/en/ui.json +++ b/packages/web/public/i18n/locales/en/ui.json @@ -66,6 +66,24 @@ "saveSuccess": { "title": "Saving Config", "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } } }, "notifications": { @@ -80,6 +98,12 @@ }, "showPassword": { "label": "Show password" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" } }, "general": { @@ -97,6 +121,9 @@ "filter": { "label": "Filter" }, + "advanced": { + "label": "Advanced" + }, "clearInput": { "label": "Clear input" }, @@ -146,6 +173,12 @@ "viaMqtt": { "label": "Connected via MQTT" }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, "language": { "label": "Language", "changeLanguage": "Change Language" diff --git a/packages/web/public/i18n/locales/es-ES/channels.json b/packages/web/public/i18n/locales/es-ES/channels.json new file mode 100644 index 00000000..12e43673 --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Canales", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Primary", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Ajustes del canal", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Role", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "Nombre", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/es-ES/commandPalette.json b/packages/web/public/i18n/locales/es-ES/commandPalette.json new file mode 100644 index 00000000..12235d7a --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Mensajes", + "map": "Mapa", + "config": "Config", + "channels": "Canales", + "nodes": "Nodes" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "Import", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/es-ES/common.json b/packages/web/public/i18n/locales/es-ES/common.json new file mode 100644 index 00000000..c566496b --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Aplique", + "backupKey": "Backup Key", + "cancel": "Cancelar", + "clearMessages": "Clear Messages", + "close": "Cerrar", + "confirm": "Confirm", + "delete": "Eliminar", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "Import", + "message": "Mensaje", + "now": "Now", + "ok": "Vale", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Quitar", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Reiniciar", + "save": "Guardar", + "scanQr": "Scan QR Code", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/es-ES/dashboard.json b/packages/web/public/i18n/locales/es-ES/dashboard.json new file mode 100644 index 00000000..9c70e841 --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Serial", + "connectionType_network": "Red", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/es-ES/deviceConfig.json b/packages/web/public/i18n/locales/es-ES/deviceConfig.json new file mode 100644 index 00000000..dfa21d26 --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Dispositivo", + "tabDisplay": "Pantalla", + "tabLora": "LoRa", + "tabNetwork": "Red", + "tabPosition": "Posición", + "tabPower": "Energía", + "tabSecurity": "Seguridad" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX Timezone" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Role" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Enabled" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Modo de emparejamiento" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Bandwidth" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Ignore MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Modem Preset" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "OK to MQTT" + }, + "overrideDutyCycle": { + "description": "Override Duty Cycle", + "label": "Override Duty Cycle" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Región" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Enabled" + }, + "gateway": { + "description": "Default Gateway", + "label": "Gateway" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Subnet" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Enabled" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "Configuración UDP" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Timestamp", + "unset": "Sin configurar", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Enable power saving mode" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Power Config" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Clave privada" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Clave Pública" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/es-ES/dialog.json b/packages/web/public/i18n/locales/es-ES/dialog.json new file mode 100644 index 00000000..149e317f --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Serial", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Connect", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Mensaje", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Tensión", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generate QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "¿Estás seguro?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "¿Estás seguro?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/es-ES/messages.json b/packages/web/public/i18n/locales/es-ES/messages.json new file mode 100644 index 00000000..fb7d1fcb --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Enviar" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Reply" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/es-ES/moduleConfig.json b/packages/web/public/i18n/locales/es-ES/moduleConfig.json new file mode 100644 index 00000000..2c192219 --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Ambient Lighting", + "tabAudio": "Audio", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Detection Sensor", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Neighbor Info", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Range Test", + "tabSerial": "Serial", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Telemetry" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Intensidad", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Red", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Green", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Blue", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Root topic", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Tiempo agotado", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Number of records", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Device metrics update interval (seconds)" + }, + "environmentUpdateInterval": { + "label": "Environment metrics update interval (seconds)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/es-ES/nodes.json b/packages/web/public/i18n/locales/es-ES/nodes.json new file mode 100644 index 00000000..6d5dec63 --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Favorito", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Error", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "Directo", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/es-ES/ui.json b/packages/web/public/i18n/locales/es-ES/ui.json new file mode 100644 index 00000000..4d4bef7c --- /dev/null +++ b/packages/web/public/i18n/locales/es-ES/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Mensajes", + "map": "Mapa", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "Canales", + "nodes": "Nodes" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Batería" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Hide password" + }, + "showPassword": { + "label": "Mostrar contraseña" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Role" + }, + "filter": { + "label": "Filtro" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Tensión" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Directo", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Última escucha", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Idioma", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "Oscuro", + "light": "Claro", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/channels.json b/packages/web/public/i18n/locales/fi-FI/channels.json new file mode 100644 index 00000000..f6d20a3e --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Kanavat", + "channelName": "Kanava: {{channelName}}", + "broadcastLabel": "Ensisijainen", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Syötä kelvollinen {{bits}} bittinen PSK." + }, + "settings": { + "label": "Kanava-asetukset", + "description": "Crypto, MQTT ja muut asetukset" + }, + "role": { + "label": "Rooli", + "description": "Laitteen telemetriatiedot lähetetään ENSISIJAISEN kanavan kautta. Vain yksi ENSISIJAINEN kanava sallitaan", + "options": { + "primary": "ENSISIJAISEN", + "disabled": "POIS KÄYTÖSTÄ", + "secondary": "TOISIJAINEN" + } + }, + "psk": { + "label": "Esijaettu avain", + "description": "Tuetut PSK-pituudet: 256-bit, 128-bit, 8-bit, tyhjät (0-bit)", + "generate": "Luo" + }, + "name": { + "label": "Nimi", + "description": "Kanavan yksilöllinen nimi (alle 12 merkkiä), jätä tyhjäksi käyttääksesi oletusta" + }, + "uplinkEnabled": { + "label": "Lähetys käytössä", + "description": "Lähetä viestejä paikallisesta verkosta MQTT-verkkoon" + }, + "downlinkEnabled": { + "label": "Vastaanotto käytössä", + "description": "Lähetä viestejä MQTT:stä paikalliseen verkkoon" + }, + "positionPrecision": { + "label": "Sijainti", + "description": "Kanavalle jaettavan sijainnin tarkkuus. Voi poistaa käytöstä.", + "options": { + "none": "Älä jaa sijaintia", + "precise": "Tarkka Sijainti", + "metric_km23": "23 kilometrin säteellä", + "metric_km12": "12 kilometrin säteellä", + "metric_km5_8": "5,8 kilometrin säteellä", + "metric_km2_9": "2,9 kilometrin säteellä", + "metric_km1_5": "1,5 kilometrin säteellä", + "metric_m700": "700 metrin säteellä", + "metric_m350": "350 metrin säteellä", + "metric_m200": "200 metrin säteellä", + "metric_m90": "90 metrin säteellä", + "metric_m50": "50 metrin säteellä", + "imperial_mi15": "15 mailin säteellä", + "imperial_mi7_3": "7,3 mailin säteellä", + "imperial_mi3_6": "3,6 mailin säteellä", + "imperial_mi1_8": "1,8 mailin säteellä", + "imperial_mi0_9": "0,9 mailin säteellä", + "imperial_mi0_5": "0,5 mailin säteellä", + "imperial_mi0_2": "0,2 mailin säteellä", + "imperial_ft600": "600 jalan säteellä", + "imperial_ft300": "300 jalan säteellä", + "imperial_ft150": "150 jalan säteellä" + } + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/commandPalette.json b/packages/web/public/i18n/locales/fi-FI/commandPalette.json new file mode 100644 index 00000000..221821eb --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "Tuloksia ei löytynyt.", + "page": { + "title": "Komentovalikko" + }, + "pinGroup": { + "label": "Kiinnitä komentoryhmä" + }, + "unpinGroup": { + "label": "Irroita komentoryhmä" + }, + "goto": { + "label": "Siirry", + "command": { + "messages": "Viestit", + "map": "Kartta", + "config": "Asetukset", + "channels": "Kanavat", + "nodes": "Laitteet" + } + }, + "manage": { + "label": "Hallinnoi", + "command": { + "switchNode": "Vaihda laitetta", + "connectNewNode": "Yhdistä uusi laite" + } + }, + "contextual": { + "label": "Kontekstin mukainen", + "command": { + "qrCode": "QR-koodi", + "qrGenerator": "QR-koodigeneraattori", + "qrImport": "Tuo", + "scheduleShutdown": "Ajasta sammutus", + "scheduleReboot": "Ajasta uudelleenkäynnistys", + "rebootToOtaMode": "Uudelleenkäynnistä OTA-tilaan", + "resetNodeDb": "Nollaa laitteen DB-tietokanta", + "factoryResetDevice": "Palauta tehdasasetukset", + "factoryResetConfig": "Tehdasasetusten palautusasetukset" + } + }, + "debug": { + "label": "Vianetsintä", + "command": { + "reconfigure": "Määritä uudelleen", + "clearAllStoredMessages": "Tyhjennä kaikki tallennetut viesti" + } + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/common.json b/packages/web/public/i18n/locales/fi-FI/common.json new file mode 100644 index 00000000..01ef9ed8 --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Hyväksy", + "backupKey": "Varmuuskopioi avain", + "cancel": "Peruuta", + "clearMessages": "Tyhjennä viestit", + "close": "Sulje", + "confirm": "Vahvista", + "delete": "Poista", + "dismiss": "Hylkää", + "download": "Lataa", + "export": "Vie", + "generate": "Luo", + "regenerate": "Luo uudelleen", + "import": "Tuo", + "message": "Viesti", + "now": "Nyt", + "ok": "OK", + "print": "Tulosta", + "rebootOtaNow": "Käynnistä uudelleen OTA-tilaan nyt", + "remove": "Poista", + "requestNewKeys": "Pyydä uudet avaimet", + "requestPosition": "Pyydä sijaintia", + "reset": "Palauta", + "save": "Tallenna", + "scanQr": "Skannaa QR-koodi", + "traceRoute": "Reitinselvitys", + "submit": "Lähetä" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Ladataan...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hyppy", + "plural": "Hyppyä" + }, + "hopsAway": { + "one": "{{count}} hypyn päässä", + "plural": "{{count}} hypyn päässä", + "unknown": "Hyppyjen määrä tuntematon" + }, + "megahertz": "MHz", + "raw": "raakatieto", + "meter": { + "one": "Metri", + "plural": "Metriä", + "suffix": "m" + }, + "minute": { + "one": "Minuutti", + "plural": "Minuuttia" + }, + "hour": { + "one": "Tunti", + "plural": "Tuntia" + }, + "millisecond": { + "one": "Millisekunti", + "plural": "Millisekuntia", + "suffix": "ms" + }, + "second": { + "one": "Sekunti", + "plural": "Sekuntia" + }, + "day": { + "one": "Päivä", + "plural": "Päivää" + }, + "month": { + "one": "Kuukausi", + "plural": "Kuukautta" + }, + "year": { + "one": "Vuosi", + "plural": "Vuotta" + }, + "snr": "SNR", + "volt": { + "one": "Voltti", + "plural": "Voltit", + "suffix": "V" + }, + "record": { + "one": "Tiedot", + "plural": "Tiedot" + } + }, + "security": { + "0bit": "Tyhjä", + "8bit": "8-bittiä", + "128bit": "128-bittiä", + "256bit": "256 bittiä" + }, + "unknown": { + "longName": "Tuntematon", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "EI ASETETTU", + "fallbackName": "Meshtastic {{last4}}", + "node": "Laite", + "formValidation": { + "unsavedChanges": "Tallentamattomat muutokset", + "tooBig": { + "string": "Teksti on liian pitkä – sallittu enimmäispituus on {{maximum}} merkkiä.", + "number": "Arvo on liian suuri – sallittu enimmäisarvo on {{maximum}}.", + "bytes": "Liian suuri koko – sallittu enimmäismäärä on {{params.maximum}} tavua." + }, + "tooSmall": { + "string": "Teksti on liian lyhyt – vähimmäispituus on {{minimum}} merkkiä.", + "number": "Arvo on liian pieni – pienin sallittu arvo on {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Virheellinen muoto – odotettu muoto on IPv4-osoite.", + "key": "Virheellinen muoto – odotettu muoto on Base64-koodattu jaettu avain (PSK)." + }, + "invalidType": { + "number": "Virheellinen tyyppi – arvon tulee olla numero." + }, + "pskLength": { + "0bit": "Avainkentän on oltava tyhjä.", + "8bit": "Avaimen on oltava 8-bittinen jaettu avain (PSK).", + "128bit": "Avaimen on oltava 128-bittinen jaettu avain (PSK).", + "256bit": "Avaimen on oltava 256-bittinen jaettu avain (PSK)." + }, + "required": { + "generic": "Tämä kenttä on pakollinen.", + "managed": "Vähintään yksi hallinta-avain vaaditaan, jos laitetta hallitaan.", + "key": "Avain on pakollinen." + } + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/dashboard.json b/packages/web/public/i18n/locales/fi-FI/dashboard.json new file mode 100644 index 00000000..43ce8a7f --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Yhdistetyt laitteet", + "description": "Hallitse yhdistettyjä Meshtastic laitteitasi.", + "connectionType_ble": "BLE", + "connectionType_serial": "Sarjaliitäntä", + "connectionType_network": "Verkko", + "noDevicesTitle": "Ei laitteita yhdistettynä", + "noDevicesDescription": "Yhdistä uusi laite aloittaaksesi.", + "button_newConnection": "Uusi yhteys" + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/deviceConfig.json b/packages/web/public/i18n/locales/fi-FI/deviceConfig.json new file mode 100644 index 00000000..8b0d01b8 --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Asetukset", + "tabBluetooth": "Bluetooth", + "tabDevice": "Laite", + "tabDisplay": "Näyttö", + "tabLora": "LoRa", + "tabNetwork": "Verkko", + "tabPosition": "Sijainti", + "tabPower": "Virta", + "tabSecurity": "Turvallisuus" + }, + "sidebar": { + "label": "Moduulit" + }, + "device": { + "title": "Laitteen asetukset", + "description": "Laitteen asetukset", + "buttonPin": { + "description": "Painikkeen pinnin ohitus", + "label": "Painikkeen pinni" + }, + "buzzerPin": { + "description": "Summerin pinnin ohitus", + "label": "Summerin pinni" + }, + "disableTripleClick": { + "description": "Poista kolmoisklikkaus käytöstä", + "label": "Poista kolmoisklikkaus käytöstä" + }, + "doubleTapAsButtonPress": { + "description": "Käsittele kaksoisnapautus painikkeen painalluksena", + "label": "Kaksoisnapautus painikkeen painalluksena" + }, + "ledHeartbeatDisabled": { + "description": "Poista ledin vilkkuminen käytöstä", + "label": "Ledin vilkkuminen poistettu käytöstä" + }, + "nodeInfoBroadcastInterval": { + "description": "Kuinka usein laitteen tiedot lähetetään verkkoon", + "label": "Laitteen tietojen lähetyksen aikaväli" + }, + "posixTimezone": { + "description": "POSIX-aikavyöhykkeen merkkijono laitetta varten", + "label": "POSIX-aikavyöhyke" + }, + "rebroadcastMode": { + "description": "Kuinka uudelleenlähetyksiä käsitellään", + "label": "Uudelleenlähetyksen tila" + }, + "role": { + "description": "Mikä rooli laitteella on mesh-verkossa", + "label": "Rooli" + } + }, + "bluetooth": { + "title": "Bluetooth asetukset", + "description": "Bluetooth moduulin asetukset", + "note": "Huomautus: Jotkin laitteet (ESP32) eivät voi käyttää bluetoothia sekä WiFiä samanaikaisesti.", + "enabled": { + "description": "Ota Bluetooth käyttöön tai poista käytöstä", + "label": "Käytössä" + }, + "pairingMode": { + "description": "PIN-koodin valinnan käyttäytyminen.", + "label": "Paritustila" + }, + "pin": { + "description": "Bluetooth PIN-koodi, jota käytetään pariliitettäessä", + "label": "PIN" + } + }, + "display": { + "description": "Laitteen näytön asetukset", + "title": "Näyttöasetukset", + "headingBold": { + "description": "Lihavoi otsikkoteksti", + "label": "Lihavoitu otsikko" + }, + "carouselDelay": { + "description": "Kuinka nopeasti ikkunat kulkevat", + "label": "Karusellin Viive" + }, + "compassNorthTop": { + "description": "Kiinnitä pohjoinen kompassin yläreunaan", + "label": "Kompassin pohjoinen ylhäällä" + }, + "displayMode": { + "description": "Näytön asettelun vaihtoehdot", + "label": "Näyttötila" + }, + "displayUnits": { + "description": "Näytä metriset tai imperiaaliset yksiköt", + "label": "Näyttöyksiköt" + }, + "flipScreen": { + "description": "Käännä näyttöä 180 astetta", + "label": "Käännä näyttö" + }, + "gpsDisplayUnits": { + "description": "Koordinaattien näyttömuoto", + "label": "GPS näyttöyksiköt" + }, + "oledType": { + "description": "Laitteeseen liitetyn OLED-näytön tyyppi", + "label": "OLED-tyyppi" + }, + "screenTimeout": { + "description": "Sammuta näyttö tämän ajan jälkeen", + "label": "Näytön aikakatkaisu" + }, + "twelveHourClock": { + "description": "Käytä 12 tunnin kelloa", + "label": "12 tunnin kello" + }, + "wakeOnTapOrMotion": { + "description": "Herätä laite napauttamalla tai liikkeestä", + "label": "Herätä napauttamalla tai liikkeellä" + } + }, + "lora": { + "title": "Mesh-verkon asetukset", + "description": "LoRa-verkon asetukset", + "bandwidth": { + "description": "Kanavan kaistanleveys MHz", + "label": "Kaistanleveys" + }, + "boostedRxGain": { + "description": "RX tehostettu vahvistus", + "label": "RX tehostettu vahvistus" + }, + "codingRate": { + "description": "Koodausnopeuden nimittäjä", + "label": "Koodausnopeus" + }, + "frequencyOffset": { + "description": "Taajuuskorjaus kalibrointivirheiden korjaamiseksi", + "label": "Taajuuspoikkeama" + }, + "frequencySlot": { + "description": "LoRa-taajuuden kanavanumero", + "label": "Taajuuspaikka" + }, + "hopLimit": { + "description": "Maksimimäärä hyppyjä", + "label": "Hyppyraja" + }, + "ignoreMqtt": { + "description": "Älä välitä MQTT-viestejä mesh-verkon yli", + "label": "Ohita MQTT" + }, + "modemPreset": { + "description": "Käytössä olevan modeemin esiasetus", + "label": "Modeemin esiasetus" + }, + "okToMqtt": { + "description": "Kun asetetaan arvoksi true, tämä asetus tarkoittaa, että käyttäjä hyväksyy paketin lähettämisen MQTT:lle. Jos asetetaan arvoksi false, etälaitteita pyydetään olemaan välittämättä paketteja MQTT:lle", + "label": "MQTT päällä" + }, + "overrideDutyCycle": { + "description": "Ohita käyttöaste (Duty Cycle)", + "label": "Ohita käyttöaste (Duty Cycle)" + }, + "overrideFrequency": { + "description": "Käytä mukautettua taajuutta", + "label": "Mukautettu taajuus" + }, + "region": { + "description": "Asettaa alueen laitteelle", + "label": "Alue" + }, + "spreadingFactor": { + "description": "Ilmaisee symbolia kohden lähetettävien taajuuksien määrän", + "label": "Levennyskerroin" + }, + "transmitEnabled": { + "description": "LoRa-radion lähetyksen (TX) käyttöönotto tai poiskytkentä", + "label": "Lähetys käytössä" + }, + "transmitPower": { + "description": "Suurin lähetysteho", + "label": "Lähetysteho" + }, + "usePreset": { + "description": "Käytä ennalta määriteltyä modeemin esiasetusta", + "label": "Käytä esiasetusta" + }, + "meshSettings": { + "description": "LoRa-verkon asetukset", + "label": "Mesh-verkon asetukset" + }, + "waveformSettings": { + "description": "LoRa-aaltomuodon asetukset", + "label": "Signaalimuodon asetukset" + }, + "radioSettings": { + "label": "Radioasetukset", + "description": "LoRa-laitteen asetukset" + } + }, + "network": { + "title": "WiFi-asetukset", + "description": "WiFi-radion asetukset", + "note": "Huomautus: Jotkin laitteet (ESP32) eivät voi käyttää sekä Bluetoothia että WiFiä samanaikaisesti.", + "addressMode": { + "description": "Osoitteen määrityksen valinta", + "label": "Osoitetila" + }, + "dns": { + "description": "DNS-palvelin", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Ota käyttöön tai poista käytöstä ethernet-portti", + "label": "Käytössä" + }, + "gateway": { + "description": "Oletusyhdyskäytävä", + "label": "Yhdyskäytävä" + }, + "ip": { + "description": "IP-osoite", + "label": "IP" + }, + "psk": { + "description": "Verkon salasana", + "label": "PSK" + }, + "ssid": { + "description": "Verkon nimi", + "label": "SSID" + }, + "subnet": { + "description": "Aliverkon peite", + "label": "Aliverkko" + }, + "wifiEnabled": { + "description": "Ota WiFi käyttöön tai poista se käytöstä", + "label": "Käytössä" + }, + "meshViaUdp": { + "label": "Mesh UDP:n kautta" + }, + "ntpServer": { + "label": "NTP-palvelin" + }, + "rsyslogServer": { + "label": "Rsyslog-palvelin" + }, + "ethernetConfigSettings": { + "description": "Ethernet-portin asetukset", + "label": "Ethernet-asetukset" + }, + "ipConfigSettings": { + "description": "IP-osoitteen asetukset", + "label": "IP-osoitteen asetukset" + }, + "ntpConfigSettings": { + "description": "NTP-asetukset", + "label": "NTP-asetukset" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog määritykset", + "label": "Rsyslog määritykset" + }, + "udpConfigSettings": { + "description": "UDP-yhdeyden asetukset", + "label": "UDP-asetukset" + } + }, + "position": { + "title": "Sijainnin asetukset", + "description": "Sijaintimoduulin asetukset", + "broadcastInterval": { + "description": "Kuinka usein sijainti lähetetään mesh-verkon yli", + "label": "Lähetyksen aikaväli" + }, + "enablePin": { + "description": "GPS-moduulin käyttöönottopinnin korvaus", + "label": "Ota pinni käytöön" + }, + "fixedPosition": { + "description": "Älä raportoi GPS-sijaintia, vaan käytä manuaalisesti määritettyä sijaintia", + "label": "Kiinteä sijainti" + }, + "gpsMode": { + "description": "Määritä, onko laitteen GPS käytössä, pois päältä vai puuttuuko se kokonaan", + "label": "GSP-tila" + }, + "gpsUpdateInterval": { + "description": "Kuinka usein GPS-paikannus suoritetaan", + "label": "GPS-päivitysväli" + }, + "positionFlags": { + "description": "Valinnaiset kentät, jotka voidaan sisällyttää sijaintiviesteihin. Mitä enemmän kenttiä valitaan, sitä suurempi viesti on, mikä johtaa pidempään lähetysaikaan ja suurempaan pakettihäviöriskiin.", + "label": "Sijaintimerkinnät" + }, + "receivePin": { + "description": "GPS-moduulin käyttöönottopinnin korvaus", + "label": "Vastaanoton pinni" + }, + "smartPositionEnabled": { + "description": "Lähetä sijainti vain, kun sijainnissa on tapahtunut merkittävä muutos", + "label": "Ota älykäs sijainti käyttöön" + }, + "smartPositionMinDistance": { + "description": "Vähimmäisetäisyys (metreinä), joka on kuljettava ennen sijaintipäivityksen lähettämistä", + "label": "Älykkään sijainnin minimietäisyys" + }, + "smartPositionMinInterval": { + "description": "Lyhin aikaväli (sekunteina), joka on kuluttava ennen sijaintipäivityksen lähettämistä", + "label": "Älykkään sijainnin vähimmäisetäisyys" + }, + "transmitPin": { + "description": "GPS-moduulin käyttöönottopinnin korvaus", + "label": "Lähetyksen pinni" + }, + "intervalsSettings": { + "description": "Kuinka usein sijaintipäivitykset lähetetään", + "label": "Aikaväli" + }, + "flags": { + "placeholder": "Valitse sijaintiasetukset...", + "altitude": "Korkeus", + "altitudeGeoidalSeparation": "Korkeuden geoidinen erotus", + "altitudeMsl": "Korkeus on mitattu merenpinnan tasosta", + "dop": "Tarkkuuden heikkenemä (DOP), oletuksena käytetään PDOP-arvoa", + "hdopVdop": "Jos DOP on asetettu, käytä HDOP- ja VDOP-arvoja PDOP:n sijaan", + "numSatellites": "Satelliittien määrä", + "sequenceNumber": "Sekvenssinumero", + "timestamp": "Aikaleima", + "unset": "Ei yhdistetty", + "vehicleHeading": "Ajoneuvon suunta", + "vehicleSpeed": "Ajoneuvon nopeus" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Käytetään akun jännitteen lukeman säätämiseen", + "label": "Korvaava AD-muuntimen kerroin" + }, + "ina219Address": { + "description": "Akkunäytön INA219 osoite", + "label": "INA219 Osoite" + }, + "lightSleepDuration": { + "description": "Kuinka kauan laite on kevyessä lepotilassa", + "label": "Kevyen lepotilan kesto" + }, + "minimumWakeTime": { + "description": "Vähimmäisaika, jonka laite pysyy hereillä paketin vastaanoton jälkeen", + "label": "Minimi heräämisaika" + }, + "noConnectionBluetoothDisabled": { + "description": "Jos laite ei saa Bluetooth-yhteyttä, BLE-radio poistetaan käytöstä tämän ajan kuluttua", + "label": "Ei yhteyttä. Bluetooth on pois käytöstä" + }, + "powerSavingEnabled": { + "description": "Valitse, jos laite saa virtaa matalavirtalähteestä (esim. aurinkopaneeli), jolloin virrankulutusta minimoidaan mahdollisimman paljon.", + "label": "Ota virransäästötila käyttöön" + }, + "shutdownOnBatteryDelay": { + "description": "Sammuta laite automaattisesti tämän ajan kuluttua akkukäytöllä, 0 tarkoittaa toistaiseksi päällä", + "label": "Viive laitteen sammuttamisessa akkukäytöllä" + }, + "superDeepSleepDuration": { + "description": "Kuinka pitkään laite on supersyvässä lepotilassa", + "label": "Supersyvän lepotilan kesto" + }, + "powerConfigSettings": { + "description": "Virtamoduulin asetukset", + "label": "Virran asetukset" + }, + "sleepSettings": { + "description": "Virtamoduulin lepotila-asetukset", + "label": "Lepotilan asetukset" + } + }, + "security": { + "description": "Turvallisuuskokoonpanon asetukset", + "title": "Turvallisuusasetukset", + "button_backupKey": "Varmuuskopioi avain", + "adminChannelEnabled": { + "description": "Salli saapuva laitteen ohjaus suojaamattoman vanhan admin-kanavan kautta", + "label": "Salli vanha admin ylläpitäjä" + }, + "enableDebugLogApi": { + "description": "Lähetä reaaliaikainen debug-loki sarjaportin kautta, katso ja vie laitteen lokitiedostot, joista sijaintitiedot on poistettu, Bluetoothin kautta", + "label": "Ota käyttöön virheenkorjauslokin API-rajapinta" + }, + "managed": { + "description": "Jos tämä on käytössä, laitteen asetuksia voi muuttaa vain etäadmin-laitteen hallintaviestien kautta. Älä ota tätä käyttöön, ellei vähintään yhtä sopivaa etäadmin-laitetta ole määritetty ja julkinen avain ei ole tallennettu johonkin yllä olevista kentistä.", + "label": "Hallinnoitu" + }, + "privateKey": { + "description": "Käytetään jaetun avaimen luomiseen etälaitteen kanssa", + "label": "Yksityinen avain" + }, + "publicKey": { + "description": "Lähetetään muille mesh-verkon laitteille, jotta ne voivat laskea jaetun salaisen avaimen", + "label": "Julkinen avain" + }, + "primaryAdminKey": { + "description": "Ensisijainen julkinen avain, jolla on oikeus lähettää hallintaviestejä tälle laitteelle", + "label": "Ensisijainen järjestelmänvalvojan avain" + }, + "secondaryAdminKey": { + "description": "Toissijainen julkinen avain, jolla on oikeus lähettää hallintaviestejä tälle laitteelle", + "label": "Toissijainen järjestelmänvalvojan avain" + }, + "serialOutputEnabled": { + "description": "Sarjakonsoli Stream API:n yli", + "label": "Sarjaulostulo Käytössä" + }, + "tertiaryAdminKey": { + "description": "Kolmas julkinen avain, jolla on oikeus lähettää hallintaviestejä tälle laitteelle", + "label": "Kolmas järjestelmänvalvojan hallinta-avain" + }, + "adminSettings": { + "description": "Järjestelmänvalvojan asetukset", + "label": "Ylläpitäjän asetukset" + }, + "loggingSettings": { + "description": "Kirjautumisen asetukset", + "label": "Kirjautumisen asetukset" + } + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/dialog.json b/packages/web/public/i18n/locales/fi-FI/dialog.json new file mode 100644 index 00000000..579dacb8 --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "Tämä toiminto poistaa kaiken viestihistorian. Toimintoa ei voi perua. Haluatko varmasti jatkaa?", + "title": "Tyhjennä kaikki viestit" + }, + "deviceName": { + "description": "Laite käynnistyy uudelleen, kun asetus on tallennettu.", + "longName": "Pitkä nimi", + "shortName": "Lyhytnimi", + "title": "Vaihda laitteen nimi" + }, + "import": { + "description": "Nykyinen LoRa-asetus ylikirjoitetaan.", + "error": { + "invalidUrl": "Virheellinen Meshtastic verkko-osoite" + }, + "channelPrefix": "Kanava: ", + "channelSetUrl": "Kanavan asetus / QR-koodin URL-osoite", + "channels": "Kanavat:", + "usePreset": "Käytä esiasetusta?", + "title": "Tuo kanava-asetukset" + }, + "locationResponse": { + "altitude": "Korkeus: ", + "coordinates": "Koordinaatit: ", + "title": "Sijainti: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Luodaanko ennalta jaettu avain uudelleen?", + "description": "Haluatko varmasti luoda ennalta jaetun avaimen uudelleen?", + "regenerate": "Luo uudelleen" + }, + "newDeviceDialog": { + "title": "Yhdistä uuteen laitteeseen", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Sarjaliitäntä", + "useHttps": "Käytä HTTPS", + "connecting": "Yhdistetään...", + "connect": "Yhdistä", + "connectionFailedAlert": { + "title": "Yhteys epäonnistui", + "descriptionPrefix": "Laitteeseen ei saatu yhteyttä. ", + "httpsHint": "Jos käytät HTTPS:ää, sinun on ehkä ensin hyväksyttävä itse allekirjoitettu varmenne. ", + "openLinkPrefix": "Avaa ", + "openLinkSuffix": " uuteen välilehteen", + "acceptTlsWarningSuffix": ", hyväksy mahdolliset TLS-varoitukset, jos niitä ilmenee ja yritä sitten uudelleen.", + "learnMoreLink": "Lue lisää" + }, + "httpConnection": { + "label": "IP-osoite / isäntänimi", + "placeholder": "000.000.000 / meshtastinen.paikallinen" + }, + "serialConnection": { + "noDevicesPaired": "Yhtään laitetta ei ole vielä yhdistetty.", + "newDeviceButton": "Uusi laite", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "Yhtään laitetta ei ole vielä yhdistetty.", + "newDeviceButton": "Uusi laite" + }, + "validation": { + "requiresWebBluetooth": "Tämä yhteystyyppi vaatii <0>Web-sarjaportti bluetooth -tuen. Käytä tuettua selainta, kuten Chromea tai Edgeä.", + "requiresWebSerial": "Tämä yhteystyyppi vaatii <0>Web-sarjaportti -tuen. Käytä tuettua selainta, kuten Chromea tai Edgeä.", + "requiresSecureContext": "Tämä sovellus vaatii <0>suojatun yhteyden. Yhdistä käyttämällä HTTPS:ää tai localhostia.", + "additionallyRequiresSecureContext": "Lisäksi se vaatii <0>suojatun yhteyden. Yhdistä käyttämällä HTTPS:ää tai localhostia." + } + }, + "nodeDetails": { + "message": "Viesti", + "requestPosition": "Pyydä sijaintia", + "traceRoute": "Reitinselvitys", + "airTxUtilization": "Ilmatien TX käyttöaste", + "allRawMetrics": "Kaikki raakatiedot:", + "batteryLevel": "Akun varaus", + "channelUtilization": "Kanavan käyttöaste", + "details": "Tiedot:", + "deviceMetrics": "Laitteen mittausloki:", + "hardware": "Laitteisto: ", + "lastHeard": "Viimeksi kuultu: ", + "nodeHexPrefix": "Laitteen Hex: !", + "nodeNumber": "Laitteen numero: ", + "position": "Sijainti:", + "role": "Rooli: ", + "uptime": "Käyttöaika: ", + "voltage": "Jännite", + "title": "Tiedot laitteelle {{identifier}}", + "ignoreNode": "Älä huomioi laitetta", + "removeNode": "Poista laite", + "unignoreNode": "Poista laitteen ohitus käytöstä" + }, + "pkiBackup": { + "loseKeysWarning": "Jos menetät avaimesi, sinun täytyy palauttaa laite tehdasasetuksiin.", + "secureBackup": "On tärkeää varmuuskopioida julkiset ja yksityiset avaimet ja säilyttää niiden varmuuskopioita turvallisesti!", + "footer": "=== AVAIMIEN LOPPU ===", + "header": "=== MESHTASTIC AVAIMET {{longName}} ({{shortName}}) LAITTEELLE ===", + "privateKey": "Yksityinen avain:", + "publicKey": "Julkinen avain:", + "fileName": "meshtastic_avaimet_{{longName}}_{{shortName}}.txt", + "title": "Varmuuskopioi avaimet" + }, + "pkiBackupReminder": { + "description": "Suosittelemme avaintietojen säännöllistä varmuuskopiointia. Haluatko varmuuskopioida nyt?", + "title": "Varmuuskopion Muistutus", + "remindLaterPrefix": "Muistuta minua", + "remindNever": "Älä muistuta minua koskaan", + "backupNow": "Varmuuskopioi nyt" + }, + "pkiRegenerate": { + "description": "Haluatko varmasti luoda avainparin uudelleen?", + "title": "Luo avainpari uudelleen" + }, + "qr": { + "addChannels": "Lisää kanavia", + "replaceChannels": "Korvaa kanavia", + "description": "Nykyinen LoRa-kokoonpano tullaan myös jakamaan.", + "sharableUrl": "Jaettava URL", + "title": "Generoi QR-koodi" + }, + "rebootOta": { + "title": "Ajasta uudelleenkäynnistys", + "description": "Käynnistä yhdistetty laite uudelleen viiveen jälkeen OTA-tilaan (langaton päivitys).", + "enterDelay": "Anna viive (sekuntia)", + "scheduled": "Uudelleenkäynnistys on ajastettu" + }, + "reboot": { + "title": "Ajasta uudelleenkäynnistys", + "description": "Käynnistä yhdistetty laite x minuutin jälkeen." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "Tämä poistaa laitteen laitteesta ja pyytää uusia avaimia.", + "keyMismatchReasonSuffix": ". Tämä johtuu siitä, että etälaitteen nykyinen julkinen avain ei vastaa aiemmin tallennettua avainta tälle laitteelle.", + "unableToSendDmPrefix": "Laitteesi ei pysty lähettämään suoraa viestiä laitteelle: " + }, + "acceptNewKeys": "Hyväksy uudet avaimet", + "title": "Avaimet eivät täsmää - {{identifier}}" + }, + "removeNode": { + "description": "Haluatko varmasti poistaa tämän laitteen?", + "title": "Poista laite?" + }, + "shutdown": { + "title": "Ajasta sammutus", + "description": "Sammuta yhdistetty laite x minuutin päästä." + }, + "traceRoute": { + "routeToDestination": "Reitin määränpää:", + "routeBack": "Reitti takaisin:" + }, + "tracerouteResponse": { + "title": "Reitinselvitys: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Kyllä, tiedän mitä teen", + "conjunction": " ja blogikirjoitukset ", + "postamble": " ja ymmärrän roolin muuttamisen vaikutukset.", + "preamble": "Olen lukenut ", + "choosingRightDeviceRole": "Valitse laitteelle oikea rooli", + "deviceRoleDocumentation": "Roolien dokumentaatio laitteelle", + "title": "Oletko varma?" + }, + "managedMode": { + "confirmUnderstanding": "Kyllä, tiedän mitä teen", + "title": "Oletko varma?", + "description": "Hallintatilan käyttöönotto estää asiakassovelluksia (mukaan lukien verkkosovellus) kirjoittamasta asetuksia radioon. Kun tila on otettu käyttöön, radion asetuksia voidaan muuttaa vain etähallintaviestien kautta. Tämä asetus ei ole pakollinen etähallittavien laitteiden hallintaan." + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/messages.json b/packages/web/public/i18n/locales/fi-FI/messages.json new file mode 100644 index 00000000..7fc4247f --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Viestit: {{chatName}}", + "placeholder": "Kirjoita viesti" + }, + "emptyState": { + "title": "Valitse keskustelu", + "text": "Ei vielä viestejä." + }, + "selectChatPrompt": { + "text": "Valitse kanava tai laite aloittaaksesi viestinnän." + }, + "sendMessage": { + "placeholder": "Kirjoita viesti tähän...", + "sendButton": "Lähetä" + }, + "actionsMenu": { + "addReactionLabel": "Lisää reaktio", + "replyLabel": "Vastaa" + }, + "deliveryStatus": { + "delivered": { + "label": "Viesti toimitettu", + "displayText": "Viesti toimitettu" + }, + "failed": { + "label": "Viestin toimitus epäonnistui", + "displayText": "Toimitus epäonnistui" + }, + "unknown": { + "label": "Viestin tila tuntematon", + "displayText": "Tuntematon tila" + }, + "waiting": { + "label": "Lähetetään viestiä", + "displayText": "Odottaa toimitusta" + } + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/moduleConfig.json b/packages/web/public/i18n/locales/fi-FI/moduleConfig.json new file mode 100644 index 00000000..2eb76dfe --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Ympäristövalaistus", + "tabAudio": "Ääni", + "tabCannedMessage": "Ennalta asetettu", + "tabDetectionSensor": "Havaitsemisanturi", + "tabExternalNotification": "Ext-ilmoitus", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Naapuritieto", + "tabPaxcounter": "PAX-laskuri", + "tabRangeTest": "Kuuluvuustesti", + "tabSerial": "Sarjaliitäntä", + "tabStoreAndForward": "S&V", + "tabTelemetry": "Telemetria" + }, + "ambientLighting": { + "title": "Ympäristövalaistuksen asetukset", + "description": "Ympäristövalaistuksen moduulin asetukset", + "ledState": { + "label": "Ledin tila", + "description": "Aseta ledi päälle tai pois päältä" + }, + "current": { + "label": "Virta", + "description": "Asettaa nykyisen ledin ulostulon. Oletus on 10" + }, + "red": { + "label": "Punainen", + "description": "Asettaa punaisen ledin tason. Arvot ovat 0-255" + }, + "green": { + "label": "Vihreä", + "description": "Asettaa vihreän ledin tason. Arvot ovat 0-255" + }, + "blue": { + "label": "Sininen", + "description": "Asettaa sinisen ledin tason. Arvot ovat 0-255" + } + }, + "audio": { + "title": "Ääniasetukset", + "description": "Äänimoduulin asetukset", + "codec2Enabled": { + "label": "Codec 2 käytössä", + "description": "Ota Codec 2 äänenkoodaus käyttöön" + }, + "pttPin": { + "label": "PTT pinni", + "description": "PTT:lle käytettävä GPIO-pinni" + }, + "bitrate": { + "label": "Tiedonsiirtonopeus", + "description": "Tiedonsiirtonopeus äänenkoodaukselle" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO-pinni jota käytetään i2S WS:ssä" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO-pinni jota käytetään i2S SD:ssä" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO-pinni jota käytetään i2S DIN:ssä" + }, + "i2sSck": { + "label": "i2S SD", + "description": "GPIO-pinni jota käytetään i2S SCK:ssa" + } + }, + "cannedMessage": { + "title": "Välitettyjen viestien asetukset", + "description": "Asetukset välitettyjen viestien moduulissa", + "moduleEnabled": { + "label": "Moduuli Käytössä", + "description": "Ota käyttöön välitetyt viestit" + }, + "rotary1Enabled": { + "label": "Kiertovalitsin #1 käytössä", + "description": "Ota kiertovalitsimen kooderi käyttöön" + }, + "inputbrokerPinA": { + "label": "Kooderin pinni A", + "description": "GPIO-pinni (1–39) kooderin portille A" + }, + "inputbrokerPinB": { + "label": "Kooderin pinni B", + "description": "GPIO-pinni (1–39) kooderin portille B" + }, + "inputbrokerPinPress": { + "label": "Kooderin pinni painallukselle", + "description": "GPIO-pinni (1–39) kooderin painallukselle" + }, + "inputbrokerEventCw": { + "label": "Myötäpäiväinen liike", + "description": "Valitse syöttötapahtuma." + }, + "inputbrokerEventCcw": { + "label": "Myötäpäiväisen liikkeen laskuri", + "description": "Valitse syöttötapahtuma." + }, + "inputbrokerEventPress": { + "label": "Painamisen tapahtuma", + "description": "Valitse syöttötapahtuma" + }, + "updown1Enabled": { + "label": "Ylös alas käytössä", + "description": "Ota käyttöön ylös / alas-suuntaa tunnistava kooderi" + }, + "allowInputSource": { + "label": "Salli syöttölaitteen lähde", + "description": "Valitse '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Lähetä äänimerkki", + "description": "Lähettää äänimerkkimerkin jokaisen viestin mukana" + } + }, + "detectionSensor": { + "title": "Tunnistinsensorin asetukset", + "description": "Tunnistinsensori-moduulin asetukset", + "enabled": { + "label": "Käytössä", + "description": "Ota käyttöön tai poista käytöstä tunnistinsensorin moduuli" + }, + "minimumBroadcastSecs": { + "label": "Minimilähetys (sekuntia)", + "description": "Aikaväli sekunteina kuinka usein viestejä voi lähettää mesh-verkkoon tilamuutoksen havaitsemisen jälkeen" + }, + "stateBroadcastSecs": { + "label": "Tilatiedon lähetys (sekuntia)", + "description": "Kuinka usein sekunteina lähetetään viesti mesh-verkkoon nykytilasta, vaikka tilassa ei olisi muutoksia" + }, + "sendBell": { + "label": "Lähetä äänimerkki", + "description": "Lähetä ASCII-äänimerkki hälytyssanoman mukana" + }, + "name": { + "label": "Käyttäjäystävälinen nimi", + "description": "Käytetään muotoilemaan mesh-verkkoon lähetettävä viesti, enintään 20 merkkiä" + }, + "monitorPin": { + "label": "Valvonta pinni", + "description": "GPIO-pinni valvonnan tilan muutoksien seurantaan" + }, + "detectionTriggerType": { + "label": "Tunnistuksen havaintotyyppi", + "description": "Käytettävän tunnistustapahtuman tyyppi" + }, + "usePullup": { + "label": "Käytä vetokytkintä (pullup)", + "description": "Käytetäänkö GPIO-pinnin INPUT_PULLUP-tilaa" + } + }, + "externalNotification": { + "title": "Ulkoisten ilmoituksien asetukset", + "description": "Määritä ulkoisten ilmoitusten moduulin asetukset", + "enabled": { + "label": "Moduuli käytössä", + "description": "Ota ulkoiset ilmoitukset käyttöön" + }, + "outputMs": { + "label": "Ulostulo MS", + "description": "Ulostulo MS" + }, + "output": { + "label": "Ulostulo", + "description": "Ulostulo" + }, + "outputVibra": { + "label": "Värinän ulostulo", + "description": "Värinän ulostulo" + }, + "outputBuzzer": { + "label": "Summerin ulostulo", + "description": "Summerin ulostulo" + }, + "active": { + "label": "Käytössä", + "description": "Käytössä" + }, + "alertMessage": { + "label": "Hälytysviesti", + "description": "Hälytysviesti" + }, + "alertMessageVibra": { + "label": "Hälytysviestin värinä", + "description": "Hälytysviestin värinä" + }, + "alertMessageBuzzer": { + "label": "Hälytysviestin summeri", + "description": "Hälytysviestin summeri" + }, + "alertBell": { + "label": "Hälytysääni", + "description": "Pitäisikö hälytyksen aktivoitua, kun vastaanotetaan äänimerkki?" + }, + "alertBellVibra": { + "label": "Hälytysäänen värinä", + "description": "Hälytysäänen värinä" + }, + "alertBellBuzzer": { + "label": "Hälytysäänen summeri", + "description": "Hälytysäänen summeri" + }, + "usePwm": { + "label": "Käytä PWM", + "description": "Käytä PWM" + }, + "nagTimeout": { + "label": "Toistokehotuksen aikakatkaisu", + "description": "Toistokehotuksen aikakatkaisu" + }, + "useI2sAsBuzzer": { + "label": "Use I²S pinniä summerille", + "description": "Määritä I²S-pinni summerin ulostuloon" + } + }, + "mqtt": { + "title": "MQTT-asetukset", + "description": "MQTT-moduulin asetukset", + "enabled": { + "label": "Käytössä", + "description": "Ota MQTT käyttöön tai poista se käytöstä" + }, + "address": { + "label": "MQTT-palvelimen osoite", + "description": "MQTT-palvelimen osoite, jota käytetään oletus- tai mukautetuissa palvelimissa" + }, + "username": { + "label": "MQTT käyttäjänimi", + "description": "MQTT käyttäjänimi, jota käytetään oletus- tai mukautetuissa palvelimissa" + }, + "password": { + "label": "MQTT salasana", + "description": "MQTT salasana, jota käytetään oletus- tai mukautetuissa palvelimissa" + }, + "encryptionEnabled": { + "label": "Salaus käytössä", + "description": "Ota MQTT-salaus käyttöön tai pois käytöstä. Huom: Jos tämä asetus ei ole käytössä, kaikki viestit lähetetään MQTT-välittäjälle salaamattomina, vaikka lähetyskanavillasi olisi salausavaimet asetettuna. Tämä koskee myös sijaintitietoja." + }, + "jsonEnabled": { + "label": "JSON käytössä", + "description": "Lähetetäänkö / otetaanko vastaan JSON-paketteja MQTT:llä" + }, + "tlsEnabled": { + "label": "TLS käytössä", + "description": "Ota TLS käyttöön tai poista se käytöstä" + }, + "root": { + "label": "Palvelimen osoite (root topic)", + "description": "MQTT-palvelimen osoite, jota käytetään oletus- tai mukautetuissa palvelimissa" + }, + "proxyToClientEnabled": { + "label": "MQTT-välityspalvelin käytössä", + "description": "Käyttää verkkoyhteyttä välittämään MQTT-viestejä asiakkaalle." + }, + "mapReportingEnabled": { + "label": "Karttaraportointi käytössä", + "description": "Laitteesi lähettää säännöllisin väliajoin salaamattoman karttaraporttipaketin määritettyyn MQTT-palvelimeen. Paketti sisältää tunnisteen, lyhyen ja pitkän nimen, likimääräisen sijainnin, laitteistomallin, roolin, laiteohjelmistoversion, LoRa-alueen, modeemin esiasetukset sekä ensisijaisen kanavan nimen." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Karttaraportoinnin aikaväli (s)", + "description": "Aikaväli sekunneissa karttaraporttien julkaisemiseksi" + }, + "positionPrecision": { + "label": "Arvioitu sijainti", + "description": "Jaetun sijainnin tarkkuus on tämän etäisyyden rajoissa", + "options": { + "metric_km23": "23 kilometrin säteellä", + "metric_km12": "12 kilometrin säteellä", + "metric_km5_8": "5,8 kilometrin säteellä", + "metric_km2_9": "2,9 kilometrin säteellä", + "metric_km1_5": "1,5 kilometrin säteellä", + "metric_m700": "700 metrin säteellä", + "metric_m350": "350 metrin säteellä", + "metric_m200": "200 metrin säteellä", + "metric_m90": "90 metrin säteellä", + "metric_m50": "50 metrin säteellä", + "imperial_mi15": "15 mailin säteellä", + "imperial_mi7_3": "7,3 mailin säteellä", + "imperial_mi3_6": "3,6 mailin säteellä", + "imperial_mi1_8": "1,8 mailin säteellä", + "imperial_mi0_9": "0,9 mailin säteellä", + "imperial_mi0_5": "0,5 mailin säteellä", + "imperial_mi0_2": "0,2 mailin säteellä", + "imperial_ft600": "600 jalan säteellä", + "imperial_ft300": "300 jalan säteellä", + "imperial_ft150": "150 jalan säteellä" + } + } + } + }, + "neighborInfo": { + "title": "Naapuritiedon asetukset", + "description": "Naapuritietojen moduulin asetukset", + "enabled": { + "label": "Käytössä", + "description": "Ota käyttöön tai poista käytöstä naapuruustietomoduuli" + }, + "updateInterval": { + "label": "Päivitysväli", + "description": "Aikaväli sekunneissa siitä, kuinka usein mesh-verkossa lähetetään naapuritietoja verkkoon" + } + }, + "paxcounter": { + "title": "Pax-laskurin asetukset", + "description": "Pax-laskurin moduulin asetukset", + "enabled": { + "label": "Moduuli käytössä", + "description": "Ota pax-laskuri käyttöön" + }, + "paxcounterUpdateInterval": { + "label": "Päivityksen aikaväli (sekuntia)", + "description": "Kuinka kauan odotetaan pax-laskurin pakettien lähettämisen välillä" + }, + "wifiThreshold": { + "label": "WiFi-RSSI kynnysarvo", + "description": "Millä WiFi RSSI tasolla laskurin täytyisi kasvaa. Oletus on -80." + }, + "bleThreshold": { + "label": "WiFi-RSSI kynnysarvo", + "description": "Millä BLE RSSI-arvolla laskuri kasvaa. Oletus: -80." + } + }, + "rangeTest": { + "title": "Kuuluvuustestin asetukset", + "description": "Kuuluvuustesti moduulin asetukset", + "enabled": { + "label": "Moduuli käytössä", + "description": "Ota kuuluvuustesti käytöön" + }, + "sender": { + "label": "Viestin aikaväli", + "description": "Kuinka kauan odotetaan testipakettien lähettämisen välillä" + }, + "save": { + "label": "Tallenna CSV tallennustilaan", + "description": "Vain ESP32" + } + }, + "serial": { + "title": "Sarjaliitännän asetukset", + "description": "Sarjaliitäntä-moduulin asetukset", + "enabled": { + "label": "Sarjaliitäntä käytössä", + "description": "Ota sarjaliitäntä käyttöön" + }, + "echo": { + "label": "Toista", + "description": "Kaikki lähettämäsi paketit toistetaan takaisin laitteellesi" + }, + "rxd": { + "label": "Vastaanota PIN-koodi", + "description": "Määritä GPIO-pinni käyttämään aiemmin asetettua RXD-pinniä." + }, + "txd": { + "label": "Lähetä PIN-koodi", + "description": "Määritä GPIO-pinni käyttämään aiemmin asetettua TXD-pinniä." + }, + "baud": { + "label": "Baud-siirtonopeus", + "description": "Sarjaliitännän baud-siirtonopeus" + }, + "timeout": { + "label": "Aikakatkaisu", + "description": "Kuinka monta sekuntia odotetaan, ennen kuin paketti merkitään valmiiksi" + }, + "mode": { + "label": "Tila", + "description": "Valitse tila" + }, + "overrideConsoleSerialPort": { + "label": "Ohita konsolin sarjaliitännän portti", + "description": "Jos sinulla on sarjaportti kytkettynä konsoliin, tämä ohittaa sen." + } + }, + "storeForward": { + "title": "Varastoi & välitä asetukset", + "description": "Varastoi & välitä moduulin asetukset", + "enabled": { + "label": "Moduuli käytössä", + "description": "Ota varastoi & välitä käyttöön" + }, + "heartbeat": { + "label": "Heartbeat käytössä", + "description": "Ota varastoi & välitä heartbeat käyttöön" + }, + "records": { + "label": "Kirjausten määrä", + "description": "Tallennettavien tietueiden määrä" + }, + "historyReturnMax": { + "label": "Historian maksimimäärä", + "description": "Enimmäismäärä tietueita palautettaviksi" + }, + "historyReturnWindow": { + "label": "Historian aikamäärä", + "description": "Enimmäismäärä tietueita palautettaviksi" + } + }, + "telemetry": { + "title": "Telemetrian asetukset", + "description": "Telemetria-moduulin asetukset", + "deviceUpdateInterval": { + "label": "Laitteen mittausloki", + "description": "Laitteen mittaustietojen päivitysväli (sekuntia)" + }, + "environmentUpdateInterval": { + "label": "Ympäristötietojen päivitysväli (sekuntia)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Moduuli käytössä", + "description": "Ota käyttöön ympäristön telemetria" + }, + "environmentScreenEnabled": { + "label": "Näytetään näytöllä", + "description": "Näytä moduulin telemetriatiedot OLED-näytöllä" + }, + "environmentDisplayFahrenheit": { + "label": "Näytä fahrenheitissa", + "description": "Näytä lämpötila fahrenheitissa" + }, + "airQualityEnabled": { + "label": "Ilmanlaatu käytössä", + "description": "Ota ilmanlaadun telemetria käyttöön" + }, + "airQualityInterval": { + "label": "Ilmanlaadun päivitysväli", + "description": "Kuinka usein ilmanlaadun tiedot lähetetään mesh-verkon kautta" + }, + "powerMeasurementEnabled": { + "label": "Tehomittaus käytössä", + "description": "Ota tehomittauksen telemetria käyttöön" + }, + "powerUpdateInterval": { + "label": "Tehomittauksen päivitysväli", + "description": "Kuinka usein tehomittauksen tiedot lähetetään mesh-verkon kautta" + }, + "powerScreenEnabled": { + "label": "Tehomittauksen näyttö käytössä", + "description": "Ota tehomittauksen telemetrian näyttö käyttöön" + } + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/nodes.json b/packages/web/public/i18n/locales/fi-FI/nodes.json new file mode 100644 index 00000000..40acfbb4 --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Julkinen avain käytössä" + }, + "noPublicKey": { + "label": "Ei julkinen avain" + }, + "directMessage": { + "label": "Suora Viesti {{shortName}}" + }, + "favorite": { + "label": "Suosikki", + "tooltip": "Lisää tai poista tämä laite suosikeistasi" + }, + "notFavorite": { + "label": "Ei suosikki" + }, + "error": { + "label": "Virhe", + "text": "Tapahtui virhe haettaessa laitteen tietoja. Yritä uudelleen myöhemmin." + }, + "status": { + "heard": "Kuultu", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Korkeus" + }, + "channelUtil": { + "label": "Kanavan käyttö" + }, + "airtimeUtil": { + "label": "Lähetysajan käyttö" + } + }, + "nodesTable": { + "headings": { + "longName": "Pitkä nimi", + "connection": "Yhteys", + "lastHeard": "Viimeksi kuultu", + "encryption": "Salaus", + "model": "Malli", + "macAddress": "MAC-osoite" + }, + "connectionStatus": { + "direct": "Suora", + "away": "poissa", + "unknown": "-", + "viaMqtt": ", MQTT välityksellä" + }, + "lastHeardStatus": { + "never": "Ei koskaan" + } + }, + "actions": { + "added": "Lisätty", + "removed": "Poistettu", + "ignoreNode": "Älä huomioi laitetta", + "unignoreNode": "Huomioi laite uudelleen", + "requestPosition": "Pyydä sijaintia" + } +} diff --git a/packages/web/public/i18n/locales/fi-FI/ui.json b/packages/web/public/i18n/locales/fi-FI/ui.json new file mode 100644 index 00000000..77c15c9d --- /dev/null +++ b/packages/web/public/i18n/locales/fi-FI/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigointi", + "messages": "Viestit", + "map": "Kartta", + "config": "Asetukset", + "radioConfig": "Radion asetukset", + "moduleConfig": "Moduulin asetukset", + "channels": "Kanavat", + "nodes": "Laitteet" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Avaa sivupalkki", + "close": "Sulje sivupalkki" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volttia", + "firmware": { + "title": "Laiteohjelmisto", + "version": "v{{version}}", + "buildDate": "Koontipäivä: {{date}}" + }, + "deviceName": { + "title": "Laitteen nimi", + "changeName": "Vaihda laitteen nimi", + "placeholder": "Anna laitteelle nimi" + }, + "editDeviceName": "Muokkaa laitteen nimeä" + } + }, + "batteryStatus": { + "charging": "{{level}} % latauksessa", + "pluggedIn": "Kytketty verkkovirtaan", + "title": "Akku" + }, + "search": { + "nodes": "Etsi laitteita...", + "channels": "Etsi kanavia...", + "commandPalette": "Etsi komentoja..." + }, + "toast": { + "positionRequestSent": { + "title": "Sijaintipyyntö lähetetty." + }, + "requestingPosition": { + "title": "Pyydetään sijaintia, odota..." + }, + "sendingTraceroute": { + "title": "Reitinselvitys lähetetty, odota..." + }, + "tracerouteSent": { + "title": "Reitinselvitys lähetetty." + }, + "savedChannel": { + "title": "Tallennettu Kanava: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Keskustelu käyttää PKI-salausta." + }, + "pskEncryption": { + "title": "Keskustelu käyttää PKI-salausta." + } + }, + "configSaveError": { + "title": "Virhe asetusten tallentamisessa", + "description": "Asetusten tallennuksessa tapahtui virhe." + }, + "validationError": { + "title": "Asetuksissa on virheitä", + "description": "Ole hyvä ja korjaa asetusvirheet ennen tallentamista." + }, + "saveSuccess": { + "title": "Tallennetaan asetukset", + "description": "Asetuksien muutos {{case}} on tallennettu." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} suosikit.", + "action": { + "added": "Lisätty", + "removed": "Poistettu", + "to": "saakka", + "from": "alkaen" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} estolista", + "action": { + "added": "Lisätty", + "removed": "Poistettu", + "to": "saakka", + "from": "alkaen" + } + } + }, + "notifications": { + "copied": { + "label": "Kopioitu!" + }, + "copyToClipboard": { + "label": "Kopioi leikepöydälle" + }, + "hidePassword": { + "label": "Piilota salasana" + }, + "showPassword": { + "label": "Näytä salasana" + }, + "deliveryStatus": { + "delivered": "Toimitettu", + "failed": "Toimitus epäonnistui", + "waiting": "Odottaa", + "unknown": "Tuntematon" + } + }, + "general": { + "label": "Yleinen" + }, + "hardware": { + "label": "Laite" + }, + "metrics": { + "label": "Mittaustiedot" + }, + "role": { + "label": "Rooli" + }, + "filter": { + "label": "Suodatus" + }, + "advanced": { + "label": "Lisäasetukset" + }, + "clearInput": { + "label": "Tyhjennä kenttä" + }, + "resetFilters": { + "label": "Tyhjennä suodattimet" + }, + "nodeName": { + "label": "Laitteen nimi tai numero", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Ajan käyttöaste (%)" + }, + "batteryLevel": { + "label": "Akun varaus (%)", + "labelText": "Akun varaus (%): {{value}}" + }, + "batteryVoltage": { + "label": "Akun jännite (V)", + "title": "Jännite" + }, + "channelUtilization": { + "label": "Kanavan käyttö (%)" + }, + "hops": { + "direct": "Suora", + "label": "Hyppyjen määrä", + "text": "Hyppyjen määrä: {{value}}" + }, + "lastHeard": { + "label": "Viimeksi kuultu", + "labelText": "Viimeksi kuultu: {{value}}", + "nowLabel": "Nyt" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Suosikit" + }, + "hide": { + "label": "Piilota" + }, + "showOnly": { + "label": "Näytä pelkästään" + }, + "viaMqtt": { + "label": "Yhdistetty MQTT-yhteydellä" + }, + "hopsUnknown": { + "label": "Tuntematon määrä hyppyjä" + }, + "showUnheard": { + "label": "Ei koskaan kuultu" + }, + "language": { + "label": "Kieli", + "changeLanguage": "Vaihda kieli" + }, + "theme": { + "dark": "Tumma", + "light": "Vaalea", + "system": "Automaattinen", + "changeTheme": "Vaihda väriteema" + }, + "errorPage": { + "title": "Tämä on hieman noloa...", + "description1": "Pahoittelemme, mutta verkkosovelluksessa tapahtui virhe, joka aiheutti kaatumisen.
\nTällaista ei pitäisi tapahtua ja me työskentelemme ahkerasti ongelman korjaamiseksi.", + "description2": "Paras tapa estää tämän tapahtuminen uudelleen sinulle tai kenellekään muulle, on ilmoittaa meille ongelmasta.", + "reportInstructions": "Lisääthän raporttiisi seuraavat tiedot:", + "reportSteps": { + "step1": "Mitä olit tekemässä virheen tapahtuessa", + "step2": "Mitä odotit tapahtuvan", + "step3": "Mitä todellisuudessa tapahtui", + "step4": "Muut mahdollisesti oleelliset tiedot" + }, + "reportLink": "Voit raportoida ongelmasta <0>GitHubissa", + "dashboardLink": "Palaa takaisin <0>hallintapaneeliin", + "detailsSummary": "Virheen tiedot", + "errorMessageLabel": "Virheilmoitus:", + "stackTraceLabel": "Virheen jäljityslista:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® on Meshtastic LLC:n rekisteröity tavaramerkki. | <1>Oikeudelliset tiedot", + "commitSha": "Ohjelmistokehitysversion SHA-tunniste: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/channels.json b/packages/web/public/i18n/locales/fr-FR/channels.json new file mode 100644 index 00000000..45c4d611 --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Canaux", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Principal", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Paramètres du canal", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Rôle", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "Nom", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/commandPalette.json b/packages/web/public/i18n/locales/fr-FR/commandPalette.json new file mode 100644 index 00000000..dcf72f2d --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Messages", + "map": "Carte", + "config": "Config", + "channels": "Canaux", + "nodes": "Noeuds" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "Import", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/common.json b/packages/web/public/i18n/locales/fr-FR/common.json new file mode 100644 index 00000000..00196840 --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Appliquer", + "backupKey": "Backup Key", + "cancel": "Annuler", + "clearMessages": "Clear Messages", + "close": "Fermer", + "confirm": "Confirm", + "delete": "Effacer", + "dismiss": "Annuler", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "Import", + "message": "Message", + "now": "Now", + "ok": "D'accord", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Supprimer", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Réinitialiser", + "save": "Sauvegarder", + "scanQr": "Scan QR Code", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Inconnu", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/dashboard.json b/packages/web/public/i18n/locales/fr-FR/dashboard.json new file mode 100644 index 00000000..3ad917f9 --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Série", + "connectionType_network": "Réseau", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/deviceConfig.json b/packages/web/public/i18n/locales/fr-FR/deviceConfig.json new file mode 100644 index 00000000..d98bc2a7 --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Appareil", + "tabDisplay": "Écran", + "tabLora": "LoRa", + "tabNetwork": "Réseau", + "tabPosition": "Position", + "tabPower": "Alimentation", + "tabSecurity": "Sécurité" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "Zone horaire POSIX" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Rôle" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Activé" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Mode d'appariement" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Bande Passante" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Ignorer MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Modem Preset" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "OK vers MQTT" + }, + "overrideDutyCycle": { + "description": "Ne pas prendre en compte la limite d'utilisation", + "label": "Ne pas prendre en compte la limite d'utilisation" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Région" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Activé" + }, + "gateway": { + "description": "Default Gateway", + "label": "Passerelle" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Sous-réseau" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Activé" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "Configuration UDP" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Horodatage", + "unset": "Désactivé", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Activer le mode économie d'énergie" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Configuration de l'alimentation" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Clé privée" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Clé publique" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/dialog.json b/packages/web/public/i18n/locales/fr-FR/dialog.json new file mode 100644 index 00000000..3440a37d --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Série", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Connect", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Message", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Tension", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Générer un QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Êtes-vous sûr ?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "Êtes-vous sûr ?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/messages.json b/packages/web/public/i18n/locales/fr-FR/messages.json new file mode 100644 index 00000000..0eb7d37b --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Envoyer" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Répondre" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/moduleConfig.json b/packages/web/public/i18n/locales/fr-FR/moduleConfig.json new file mode 100644 index 00000000..c9675a27 --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Lumière ambiante", + "tabAudio": "Audio", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Capteur de détection", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Informations sur les voisins", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Test de portée", + "tabSerial": "Série", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Télémetrie (Capteurs)" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Actif", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Rouge", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Vert", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Bleu", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Activé", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Activé", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Sujet principal", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Activé", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Écho", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Délai d'expiration", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Nombre d'enregistrements", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Intervalle de mise à jour des métriques de l'appareil (secondes)" + }, + "environmentUpdateInterval": { + "label": "Intervalle de mise à jour des métriques d'environnement (secondes)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/nodes.json b/packages/web/public/i18n/locales/fr-FR/nodes.json new file mode 100644 index 00000000..07375418 --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Favoris", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Erreur", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Capté", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "Direk", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/fr-FR/ui.json b/packages/web/public/i18n/locales/fr-FR/ui.json new file mode 100644 index 00000000..0d14f2bf --- /dev/null +++ b/packages/web/public/i18n/locales/fr-FR/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Messages", + "map": "Carte", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "Canaux", + "nodes": "Noeuds" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Batterie" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Masquer le mot de passe" + }, + "showPassword": { + "label": "Afficher le mot de passe" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "En attente . . .", + "unknown": "Inconnu" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Matériel" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Rôle" + }, + "filter": { + "label": "Filtre" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Tension" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Direk", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Dernière écoute", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Langue", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "Sombre", + "light": "Clair", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/it-IT/channels.json b/packages/web/public/i18n/locales/it-IT/channels.json new file mode 100644 index 00000000..5f3480d9 --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Canali", + "channelName": "Canale {{channelName}}", + "broadcastLabel": "Principale", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Per favore inserisci un PSK valido di {{bits}} bit." + }, + "settings": { + "label": "Impostazioni Canale", + "description": "Impostazioni di Crypto, MQTT e varie" + }, + "role": { + "label": "Ruolo", + "description": "La telemetria del dispositivo è inviata su PRIMARIO. Solo un PRIMARIO è consentito", + "options": { + "primary": "PRIMARIO", + "disabled": "DISABILITATO", + "secondary": "SECONDARIO" + } + }, + "psk": { + "label": "Chiave Pre-Condivisa", + "description": "Lunghezze PSK supportate: 256-bit, 128-bit, 8-bit, Vuoto (0-bit)", + "generate": "Genera" + }, + "name": { + "label": "Nome", + "description": "Un nome univoco per il canale <12 byte, lascia vuoto per default" + }, + "uplinkEnabled": { + "label": "Uplink Abilitato", + "description": "Invia messaggi dalla mesh locale a MQTT" + }, + "downlinkEnabled": { + "label": "Uplink Abilitato", + "description": "Invia messaggi da MQTT alla mesh locale" + }, + "positionPrecision": { + "label": "Posizione", + "description": "La precisione della posizione da condividere con il canale. Può essere disabilitata.", + "options": { + "none": "Non condividere la posizione", + "precise": "Posizione precisa", + "metric_km23": "Entro 23 chilometri", + "metric_km12": "Entro 12 chilometri", + "metric_km5_8": "Entro 5,8 chilometri", + "metric_km2_9": "Entro 2,9 chilometri", + "metric_km1_5": "Entro 1,5 chilometri", + "metric_m700": "Entro 700 metri", + "metric_m350": "Entro 350 metri", + "metric_m200": "Entro 200 metri", + "metric_m90": "Entro 90 metri", + "metric_m50": "Entro 50 metri", + "imperial_mi15": "Entro 15 miglia", + "imperial_mi7_3": "Entro 7,3 miglia", + "imperial_mi3_6": "Entro 3,6 miglia", + "imperial_mi1_8": "Entro 1,8 miglia", + "imperial_mi0_9": "Entro 0,9 miglia", + "imperial_mi0_5": "Entro 0,5 miglia", + "imperial_mi0_2": "Entro 0,2 miglia", + "imperial_ft600": "Entro 600 piedi", + "imperial_ft300": "Entro 300 piedi", + "imperial_ft150": "Entro 150 piedi" + } + } +} diff --git a/packages/web/public/i18n/locales/it-IT/commandPalette.json b/packages/web/public/i18n/locales/it-IT/commandPalette.json new file mode 100644 index 00000000..9f80b57e --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "Nessun risultato trovato.", + "page": { + "title": "Menu Comandi" + }, + "pinGroup": { + "label": "Fissa gruppo comandi" + }, + "unpinGroup": { + "label": "Rimuovi gruppo comandi" + }, + "goto": { + "label": "Vai a", + "command": { + "messages": "Messaggi", + "map": "Mappa", + "config": "Configurazione", + "channels": "Canali", + "nodes": "Nodi" + } + }, + "manage": { + "label": "Gestisci", + "command": { + "switchNode": "Cambia Nodo", + "connectNewNode": "Connetti Nuovo Nodo" + } + }, + "contextual": { + "label": "Contestuale", + "command": { + "qrCode": "Codice QR", + "qrGenerator": "Generatore", + "qrImport": "Importa", + "scheduleShutdown": "Pianifica Spegnimento", + "scheduleReboot": "Pianifica Riavvio", + "rebootToOtaMode": "Riavvia In Modalità OTA", + "resetNodeDb": "Resetta DB dei Nodi", + "factoryResetDevice": "Factory reset dispositivo", + "factoryResetConfig": "Factory reset impostazioni" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Riconfigura", + "clearAllStoredMessages": "Cancella Tutti i Messaggi Memorizzati" + } + } +} diff --git a/packages/web/public/i18n/locales/it-IT/common.json b/packages/web/public/i18n/locales/it-IT/common.json new file mode 100644 index 00000000..f8664c86 --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Applica", + "backupKey": "Backup della chiave", + "cancel": "Annulla", + "clearMessages": "Cancella Messaggi", + "close": "Chiudi", + "confirm": "Conferma", + "delete": "Elimina", + "dismiss": "Annulla", + "download": "Scarica", + "export": "Esporta", + "generate": "Genera", + "regenerate": "Rigenera", + "import": "Importa", + "message": "Messaggio", + "now": "Adesso", + "ok": "Ok", + "print": "Stampa", + "rebootOtaNow": "Riavvia ora in modalità OTA", + "remove": "Elimina", + "requestNewKeys": "Richiedi Nuove Chiavi", + "requestPosition": "Richiedi posizione", + "reset": "Reset", + "save": "Salva", + "scanQr": "Scansiona codice QR", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Caricamento in corso...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "A {{count}} hop di distanza", + "plural": "A {{count}} hop di distanza", + "unknown": "Hop sconosciuti" + }, + "megahertz": "MHz", + "raw": "grezzo", + "meter": { + "one": "Metro", + "plural": "Metri", + "suffix": "m" + }, + "minute": { + "one": "Minuto", + "plural": "Minuti" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecondo", + "plural": "Millisecondi", + "suffix": "ms" + }, + "second": { + "one": "Secondo", + "plural": "Secondi" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volt", + "suffix": "V" + }, + "record": { + "one": "Record", + "plural": "Record" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Sconosciuto", + "shortName": "SCON", + "notAvailable": "N/D", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "ANNULLA", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Troppo lungo, atteso minore o uguale a {{maximum}} caratteri.", + "number": "Troppo grande, ci si aspetta un numero minore o uguale a {{maximum}}.", + "bytes": "Troppo grande, atteso inferiore o uguale a {{params.maximum}} byte." + }, + "tooSmall": { + "string": "Troppo breve, atteso più di o uguale a {{minimum}} caratteri.", + "number": "Troppo piccolo, atteso un numero maggiore o uguale a {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Formato non valido, previsto un indirizzo IPv4.", + "key": "Formato non valido, prevista una chiave pre-condivisa codificata Base64 (PSK)." + }, + "invalidType": { + "number": "Tipo non valido, atteso un numero." + }, + "pskLength": { + "0bit": "La chiave deve essere vuota.", + "8bit": "La chiave deve essere una chiave pre-condivisa a 8 bit (PSK).", + "128bit": "La chiave deve essere una chiave pre-condivisa a 128 bit (PSK).", + "256bit": "La chiave deve essere una chiave pre-condivisa a 256 bit (PSK)." + }, + "required": { + "generic": "Questo campo è obbligatorio.", + "managed": "Se il nodo è gestito, è richiesta almeno una chiave amministrativa.", + "key": "La chiave è obbligatoria." + } + } +} diff --git a/packages/web/public/i18n/locales/it-IT/dashboard.json b/packages/web/public/i18n/locales/it-IT/dashboard.json new file mode 100644 index 00000000..c31423a9 --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Dispositivi Connessi", + "description": "Gestisci i tuoi dispositivi Meshtastic collegati.", + "connectionType_ble": "BLE", + "connectionType_serial": "Seriale", + "connectionType_network": "Rete", + "noDevicesTitle": "Nessun dispositivo connesso", + "noDevicesDescription": "Connetti un nuovo dispositivo per iniziare.", + "button_newConnection": "Nuova connessione" + } +} diff --git a/packages/web/public/i18n/locales/it-IT/deviceConfig.json b/packages/web/public/i18n/locales/it-IT/deviceConfig.json new file mode 100644 index 00000000..bd89dd07 --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configurazione", + "tabBluetooth": "Bluetooth", + "tabDevice": "Dispositivo", + "tabDisplay": "Schermo", + "tabLora": "LoRa", + "tabNetwork": "Rete", + "tabPosition": "Posizione", + "tabPower": "Alimentazione", + "tabSecurity": "Sicurezza" + }, + "sidebar": { + "label": "Moduli" + }, + "device": { + "title": "Impostazioni del dispositivo", + "description": "Le impostazioni del dispositivo", + "buttonPin": { + "description": "Sovrascrivi pin pulsante", + "label": "Pulsante Pin" + }, + "buzzerPin": { + "description": "Sovrascrivi pin buzzer", + "label": "Pin Buzzer" + }, + "disableTripleClick": { + "description": "Disabilita triplo-click", + "label": "Disabilita triplo-click" + }, + "doubleTapAsButtonPress": { + "description": "Tratta il doppio tocco come pressione pulsante", + "label": "Doppio tocco come pressione pulsante" + }, + "ledHeartbeatDisabled": { + "description": "Disabilita il LED lampeggiante predefinito", + "label": "LED Heartbeat Disabilitato" + }, + "nodeInfoBroadcastInterval": { + "description": "Quante volte trasmettere informazioni sul nodo", + "label": "Intervallo Di Trasmissione Info Nodo" + }, + "posixTimezone": { + "description": "La stringa di fuso orario POSIX per il dispositivo", + "label": "POSIX Timezone" + }, + "rebroadcastMode": { + "description": "Come gestire il rebroadcasting", + "label": "Modalità Rebroadcast" + }, + "role": { + "description": "Quale ruolo il dispositivo svolge sulla mesh", + "label": "Ruolo" + } + }, + "bluetooth": { + "title": "Impostazioni Bluetooth", + "description": "Impostazioni per il modulo Bluetooth", + "note": "Nota: alcuni dispositivi (ESP32) non possono utilizzare contemporaneamente Bluetooth e WiFi.", + "enabled": { + "description": "Abilita o disabilita Bluetooth", + "label": "Abilitato" + }, + "pairingMode": { + "description": "Comportamento di selezione pin.", + "label": "Modalità abbinamento" + }, + "pin": { + "description": "Pin da usare durante l'abbinamento", + "label": "Pin" + } + }, + "display": { + "description": "Impostazioni per il display del dispositivo", + "title": "Impostazioni Display", + "headingBold": { + "description": "Metti in grassetto il testo dell'intestazione", + "label": "Intestazione in grassetto" + }, + "carouselDelay": { + "description": "Quanto velocemente scorrere attraverso le finestre", + "label": "Ritardo Carosello" + }, + "compassNorthTop": { + "description": "Fissare il nord fino alla parte superiore della bussola", + "label": "Bussola Nord In Alto" + }, + "displayMode": { + "description": "Variante layout schermo", + "label": "Modalità di visualizzazione" + }, + "displayUnits": { + "description": "Mostra unità metriche o imperiali", + "label": "Mostra Unità" + }, + "flipScreen": { + "description": "Rifletti la visualizzazione di 180 gradi", + "label": "Capovolgi schermo" + }, + "gpsDisplayUnits": { + "description": "Formato coordinate", + "label": "Unità Display GPS" + }, + "oledType": { + "description": "Tipo di schermo OLED collegato al dispositivo", + "label": "Tipo OLED" + }, + "screenTimeout": { + "description": "Spegni lo schermo dopo questo lungo periodo", + "label": "Timeout Schermo" + }, + "twelveHourClock": { + "description": "Usa formato orologio 12 ore", + "label": "Orologio 12 Ore" + }, + "wakeOnTapOrMotion": { + "description": "Risveglia il dispositivo al tocco o al movimento", + "label": "Sveglia al tocco o al movimento" + } + }, + "lora": { + "title": "Impostazioni Mesh", + "description": "Impostazioni per la mesh LoRa", + "bandwidth": { + "description": "Larghezza di banda del canale in MHz", + "label": "Larghezza di banda" + }, + "boostedRxGain": { + "description": "Guadagno RX potenziato", + "label": "Guadagno RX potenziato" + }, + "codingRate": { + "description": "Il denominatore della velocità di codifica", + "label": "Velocità di codifica" + }, + "frequencyOffset": { + "description": "Offset di frequenza per correggere gli errori di calibrazione del cristallo", + "label": "Offset Di Frequenza" + }, + "frequencySlot": { + "description": "Numero canale di frequenza LoRa", + "label": "Slot di frequenza" + }, + "hopLimit": { + "description": "Numero massimo di hop", + "label": "Limite di hop" + }, + "ignoreMqtt": { + "description": "Non inoltrare i messaggi MQTT sulla mesh", + "label": "Ignora MQTT" + }, + "modemPreset": { + "description": "Preimpostazione modem da usare", + "label": "Configurazione Modem" + }, + "okToMqtt": { + "description": "Quando impostato su true, questa configurazione indica che l'utente approva il caricamento del pacchetto su MQTT. Se impostato su false, ai nodi remoti viene richiesto di non inoltrare i pacchetti a MQTT", + "label": "OK per MQTT" + }, + "overrideDutyCycle": { + "description": "Ignora limite di Duty Cycle", + "label": "Ignora limite di Duty Cycle" + }, + "overrideFrequency": { + "description": "Sovrascrivi frequenza", + "label": "Sovrascrivi frequenza" + }, + "region": { + "description": "Imposta la regione del tuo nodo", + "label": "Regione" + }, + "spreadingFactor": { + "description": "Indica il numero di chirp per simbolo", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Abilita/Disabilita la trasmissione (TX) dalla radio LoRa", + "label": "Trasmissione Abilitata" + }, + "transmitPower": { + "description": "Potenza massima di trasmissione", + "label": "Potenza Di Trasmissione" + }, + "usePreset": { + "description": "Usa una delle preimpostazioni predefinite del modem", + "label": "Utilizza il preset" + }, + "meshSettings": { + "description": "Impostazioni per la mesh LoRa", + "label": "Impostazioni Mesh" + }, + "waveformSettings": { + "description": "Impostazioni per la forma d'onda LoRa", + "label": "Impostazioni Forma d'onda" + }, + "radioSettings": { + "label": "Impostazioni Radio", + "description": "Impostazioni per la radio LoRa" + } + }, + "network": { + "title": "Configurazione WiFi", + "description": "Configurazione radio WiFi", + "note": "Nota: alcuni dispositivi (ESP32) non possono utilizzare contemporaneamente Bluetooth e WiFi.", + "addressMode": { + "description": "Selezione assegnazione indirizzo", + "label": "Modalità Indirizzo" + }, + "dns": { + "description": "Server DNS", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Abilita o disabilita la porta Ethernet", + "label": "Abilitato" + }, + "gateway": { + "description": "Gateway predefinito", + "label": "Gateway" + }, + "ip": { + "description": "Indirizzo IP", + "label": "IP" + }, + "psk": { + "description": "Password di rete", + "label": "PSK" + }, + "ssid": { + "description": "Nome rete", + "label": "SSID" + }, + "subnet": { + "description": "Subnet mask", + "label": "Subnet" + }, + "wifiEnabled": { + "description": "Abilita o disabilita la radio WiFi", + "label": "Abilitato" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "Server NTP" + }, + "rsyslogServer": { + "label": "Server rsyslog" + }, + "ethernetConfigSettings": { + "description": "Configurazione porta Ethernet", + "label": "Configurazione Ethernet" + }, + "ipConfigSettings": { + "description": "Configurazione IP", + "label": "Configurazione IP" + }, + "ntpConfigSettings": { + "description": "Configurazione NTP", + "label": "Configurazione NTP" + }, + "rsyslogConfigSettings": { + "description": "Configurazione di Rsyslog", + "label": "Configurazione di Rsyslog" + }, + "udpConfigSettings": { + "description": "Configurazione UDP over Mesh", + "label": "Configurazione UDP" + } + }, + "position": { + "title": "Impostazioni Posizione", + "description": "Impostazioni per il modulo di posizione", + "broadcastInterval": { + "description": "Quante volte la tua posizione viene inviata attraverso la mesh", + "label": "Intervallo Di Trasmissione" + }, + "enablePin": { + "description": "Modulo GPS abilita pin override", + "label": "Abilita Pin" + }, + "fixedPosition": { + "description": "Non segnalare la posizione GPS, ma specificane una manualmente", + "label": "Posizione Fissa" + }, + "gpsMode": { + "description": "Configura se il GPS del dispositivo è abilitato, disabilitato o non presente", + "label": "Modalità GPS" + }, + "gpsUpdateInterval": { + "description": "Quante volte una correzione GPS dovrebbe essere acquisita", + "label": "Intervallo di aggiornamento GPS" + }, + "positionFlags": { + "description": "Campi opzionali da includere quando si assemblano messaggi di posizione. Più i campi sono selezionati, più grande sarà il messaggio porterà a un utilizzo più lungo del tempo di volo e un rischio più elevato di perdite dei pacchetti.", + "label": "Flag Di Posizione" + }, + "receivePin": { + "description": "Sovrascrivi RX pin del Modulo GPS", + "label": "Pin di ricezione" + }, + "smartPositionEnabled": { + "description": "Invia posizione solo quando c'è stato un cambiamento significativo nella posizione", + "label": "Abilita Posizione Intelligente" + }, + "smartPositionMinDistance": { + "description": "Distanza minima (in metri) che deve essere percorsa prima di inviare un aggiornamento di posizione", + "label": "Distanza Minima Posizione Intelligente" + }, + "smartPositionMinInterval": { + "description": "Intervallo minimo (in secondi) che deve passare prima di inviare un aggiornamento della posizione", + "label": "Intervallo Minimo Posizione Intelligente" + }, + "transmitPin": { + "description": "Sovrascrivi TX pin del Modulo GPS", + "label": "Pin di trasmissione" + }, + "intervalsSettings": { + "description": "Quante volte inviare aggiornamenti di posizione", + "label": "Intervalli" + }, + "flags": { + "placeholder": "Seleziona flag di posizione...", + "altitude": "Altitudine", + "altitudeGeoidalSeparation": "Altitudine Separazione Geoidale", + "altitudeMsl": "L'altitudine è riferita al livello medio del mare", + "dop": "Diluizione del PDOP di precisione (DOP) utilizzato per impostazione predefinita", + "hdopVdop": "Se DOP è impostato, usa i valori HDOP / VDOP invece di PDOP", + "numSatellites": "Numero di satelliti", + "sequenceNumber": "Numero sequenza", + "timestamp": "Data e ora", + "unset": "Non impostato", + "vehicleHeading": "Direzione del veicolo", + "vehicleSpeed": "Velocità del veicolo" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Utilizzato per modificare la lettura della tensione della batteria", + "label": "Sovrascrivi rapporto moltiplicatore ADC" + }, + "ina219Address": { + "description": "Indirizzo del monitor batteria INA219", + "label": "Indirizzo INA219" + }, + "lightSleepDuration": { + "description": "Per quanto tempo il dispositivo sarà in light sleep", + "label": "Durata light sleep" + }, + "minimumWakeTime": { + "description": "Quantità minima di tempo che il dispositivo rimarrà attivo per dopo aver ricevuto un pacchetto", + "label": "Tempo Di Risveglio Minimo" + }, + "noConnectionBluetoothDisabled": { + "description": "Se il dispositivo non riceve una connessione Bluetooth, la radio BLE sarà disattivata dopo questo lungo periodo", + "label": "Nessuna Connessione Bluetooth Disabilitata" + }, + "powerSavingEnabled": { + "description": "Selezionare se alimentato da una fonte a bassa corrente (cioè solare) per minimizzare il consumo energetico il più possibile.", + "label": "Abilita modalità risparmio energetico" + }, + "shutdownOnBatteryDelay": { + "description": "Spegnimento automatico del nodo dopo questo lungo quando la batteria, 0 per indefinito", + "label": "Arresto in ritardo della batteria" + }, + "superDeepSleepDuration": { + "description": "Per quanto tempo il dispositivo sarà in deep sleep", + "label": "Durata Super Deep Sleep" + }, + "powerConfigSettings": { + "description": "Impostazioni per il modulo di alimentazione", + "label": "Configurazione Alimentazione" + }, + "sleepSettings": { + "description": "Impostazioni di sospensione per il modulo di alimentazione", + "label": "Impostazioni Sleep" + } + }, + "security": { + "description": "Impostazioni per la configurazione di sicurezza", + "title": "Impostazioni di Sicurezza", + "button_backupKey": "Chiave Di Backup", + "adminChannelEnabled": { + "description": "Consenti il controllo del dispositivo in arrivo sul canale di amministrazione ereditato insicuro", + "label": "Consenti Amministrazione Legacy" + }, + "enableDebugLogApi": { + "description": "Output live debug logging su seriale, visualizza ed esporta i log di posizione dei dispositivi redatti su Bluetooth", + "label": "Abilita Debug Log API" + }, + "managed": { + "description": "Se abilitata, le opzioni di configurazione del dispositivo possono essere modificate solo in remoto da un nodo di amministrazione remota tramite messaggi amministratori. Non abilitare questa opzione a meno che non sia stato configurato almeno un nodo Admin Remoto adatto, e la chiave pubblica è memorizzata in uno dei campi precedenti.", + "label": "Gestito" + }, + "privateKey": { + "description": "Usato per creare una chiave condivisa con un dispositivo remoto", + "label": "Chiave Privata" + }, + "publicKey": { + "description": "Inviato ad altri nodi sulla mesh per consentire loro di calcolare una chiave segreta condivisa", + "label": "Chiave Pubblica" + }, + "primaryAdminKey": { + "description": "La chiave pubblica primaria autorizzata a inviare messaggi di amministrazione a questo nodo", + "label": "Chiave Primaria Dell'Amministratore" + }, + "secondaryAdminKey": { + "description": "La chiave pubblica secondaria autorizzata a inviare messaggi di amministrazione a questo nodo", + "label": "Chiave Secondaria Dell'Amministratore" + }, + "serialOutputEnabled": { + "description": "Console seriale attraverso la Stream API", + "label": "Output Seriale Abilitato" + }, + "tertiaryAdminKey": { + "description": "La chiave pubblica terziaria autorizzata a inviare messaggi di amministrazione a questo nodo", + "label": "Chiave Di Amministrazione Terziaria" + }, + "adminSettings": { + "description": "Impostazioni per Amministratore", + "label": "Impostazioni Amministratore" + }, + "loggingSettings": { + "description": "Impostazioni per il logging", + "label": "Impostazioni Di Logging" + } + } +} diff --git a/packages/web/public/i18n/locales/it-IT/dialog.json b/packages/web/public/i18n/locales/it-IT/dialog.json new file mode 100644 index 00000000..402b6838 --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "Questa azione cancellerà tutta la cronologia dei messaggi. Non può essere annullata. Sei sicuro di voler continuare?", + "title": "Cancella Tutti I Messaggi" + }, + "deviceName": { + "description": "Il dispositivo verrà riavviato una volta salvata la configurazione.", + "longName": "Nome Lungo", + "shortName": "Nome Breve", + "title": "Cambia Nome Dispositivo" + }, + "import": { + "description": "La configurazione attuale di LoRa sarà sovrascritta.", + "error": { + "invalidUrl": "URL Meshtastic non valido" + }, + "channelPrefix": "Canale: ", + "channelSetUrl": "URL del Set di Canali/Codice QR", + "channels": "Canali:", + "usePreset": "Utilizza il preset?", + "title": "Importa Set Canale" + }, + "locationResponse": { + "altitude": "Altitudine: ", + "coordinates": "Coordinate: ", + "title": "Posizione: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Rigenerare La Chiave Pre-Condivisa?", + "description": "Sei sicuro di voler rigenerare la chiave pre-condivisa?", + "regenerate": "Rigenera" + }, + "newDeviceDialog": { + "title": "Connetti Nuovo Dispositivo", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Seriale", + "useHttps": "Usa HTTPS", + "connecting": "Connessione in corso...", + "connect": "Connetti", + "connectionFailedAlert": { + "title": "Connessione fallita", + "descriptionPrefix": "Impossibile connettersi al dispositivo.", + "httpsHint": "Se si utilizza HTTPS, potrebbe essere necessario prima accettare un certificato autofirmato. ", + "openLinkPrefix": "Apri per favore ", + "openLinkSuffix": " in una nuova scheda", + "acceptTlsWarningSuffix": ", accetta eventuali avvisi TLS se richiesto, quindi riprova", + "learnMoreLink": "Maggiori informazioni" + }, + "httpConnection": { + "label": "Indirizzo IP/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "Nessun dispositivo ancora abbinato.", + "newDeviceButton": "Nuovo dispositivo", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "Nessun dispositivo ancora abbinato.", + "newDeviceButton": "Nuovo dispositivo" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "Questa applicazione richiede un <0>contesto sicuro. Si prega di connettersi utilizzando HTTPS o localhost.", + "additionallyRequiresSecureContext": "Inoltre, richiede un <0>contesto sicuro. Si prega di connettersi utilizzando HTTPS o localhost." + } + }, + "nodeDetails": { + "message": "Messaggio", + "requestPosition": "Richiedi posizione", + "traceRoute": "Trace Route", + "airTxUtilization": "Tempo di Trasmissione Utilizzato", + "allRawMetrics": "Tutte Le Metriche Grezze:", + "batteryLevel": "Livello batteria", + "channelUtilization": "Utilizzo Canale", + "details": "Dettagli:", + "deviceMetrics": "Metriche Dispositivo:", + "hardware": "Hardware: ", + "lastHeard": "Ultimo Contatto: ", + "nodeHexPrefix": "Nodo Hex: !", + "nodeNumber": "Numero Nodo: ", + "position": "Posizione:", + "role": "Ruolo: ", + "uptime": "Tempo di attività: ", + "voltage": "Tensione", + "title": "Dettagli nodo per {{identifier}}", + "ignoreNode": "Ignora nodo", + "removeNode": "Rimuovi Nodo", + "unignoreNode": "Non ignorare più nodo" + }, + "pkiBackup": { + "loseKeysWarning": "Se perdi le tue chiavi, dovrai reimpostare il tuo dispositivo.", + "secureBackup": "È importante eseguire il backup delle chiavi pubbliche e private e memorizzare il backup in modo sicuro!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Chiave Privata:", + "publicKey": "Chiave Pubblica:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup chiavi" + }, + "pkiBackupReminder": { + "description": "Ti consigliamo di eseguire regolarmente il backup delle tue chiavi. Vuoi eseguire un backup?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Sei sicuro di voler rigenerare la coppia di chiavi?", + "title": "Rigenera Coppia Chiavi" + }, + "qr": { + "addChannels": "Aggiungi canali", + "replaceChannels": "Sostituisci Canali", + "description": "Anche l'attuale configurazione di LoRa verrà condivisa.", + "sharableUrl": "URL Condivisibile", + "title": "Genera codice QR" + }, + "rebootOta": { + "title": "Pianifica Riavvio", + "description": "Riavvia il nodo connesso dopo un ritardo in modalità OTA (Over-the-Air).", + "enterDelay": "Inserisci ritardo (sec)", + "scheduled": "Il riavvio è stato pianificato" + }, + "reboot": { + "title": "Pianifica Riavvio", + "description": "Riavvia il nodo connesso dopo x minuti." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "Questo rimuoverà il nodo dal dispositivo e richiederà nuove chiavi.", + "keyMismatchReasonSuffix": ". Ciò è dovuto alla chiave pubblica corrente del nodo remoto che non corrisponde alla chiave precedentemente memorizzata per questo nodo.", + "unableToSendDmPrefix": "Il nodo non è in grado di inviare un messaggio diretto al nodo: " + }, + "acceptNewKeys": "Accetta Nuove Chiavi", + "title": "Chiavi Non Corrispondenti - {{identifier}}" + }, + "removeNode": { + "description": "Sei sicuro di voler rimuovere questo nodo?", + "title": "Rimuovi Nodo?" + }, + "shutdown": { + "title": "Pianifica Spegnimento", + "description": "Spegni il nodo connesso dopo x minuti." + }, + "traceRoute": { + "routeToDestination": "Percorso di destinazione:", + "routeBack": "Percorso indietro:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Sì, so cosa sto facendo", + "conjunction": " e il blog post su ", + "postamble": " e capisco le implicazioni di un cambiamento di ruolo.", + "preamble": "Dichiaro di aver letto i ", + "choosingRightDeviceRole": "Scegliere il ruolo corretto del dispositivo", + "deviceRoleDocumentation": "Documentazione sul ruolo del dispositivo", + "title": "Sei sicuro?" + }, + "managedMode": { + "confirmUnderstanding": "Sì, so cosa sto facendo", + "title": "Sei sicuro?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/it-IT/messages.json b/packages/web/public/i18n/locales/it-IT/messages.json new file mode 100644 index 00000000..7a8cd6bd --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messaggi: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Seleziona una chat", + "text": "Ancora nessun messaggio." + }, + "selectChatPrompt": { + "text": "Selezionare un canale o un nodo per iniziare a messaggiare." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Invia" + }, + "actionsMenu": { + "addReactionLabel": "Aggiungi una Reazione", + "replyLabel": "Rispondi" + }, + "deliveryStatus": { + "delivered": { + "label": "Messaggio inviato", + "displayText": "Messaggio inviato" + }, + "failed": { + "label": "Invio messaggio non riuscito", + "displayText": "Invio fallito" + }, + "unknown": { + "label": "Stato messaggio sconosciuto", + "displayText": "Stato sconosciuto" + }, + "waiting": { + "label": "Inviando il messaggio...", + "displayText": "In attesa della consegna" + } + } +} diff --git a/packages/web/public/i18n/locales/it-IT/moduleConfig.json b/packages/web/public/i18n/locales/it-IT/moduleConfig.json new file mode 100644 index 00000000..c868c00b --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Luce Ambientale", + "tabAudio": "Audio", + "tabCannedMessage": "Predefiniti", + "tabDetectionSensor": "Sensore Di Rilevamento", + "tabExternalNotification": "Notifica Esterna", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Informazioni Vicinato", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Test Distanza", + "tabSerial": "Seriale", + "tabStoreAndForward": "S&I", + "tabTelemetry": "Telemetria" + }, + "ambientLighting": { + "title": "Configurazione Illuminazione Ambientale", + "description": "Impostazioni per il modulo Illuminazione Ambientale", + "ledState": { + "label": "Stato LED", + "description": "Imposta LED su acceso o spento" + }, + "current": { + "label": "Attuale", + "description": "Imposta la corrente per l'uscita LED. Il valore predefinito è 10" + }, + "red": { + "label": "Rosso", + "description": "Imposta il livello rosso del LED. I valori sono 0-255" + }, + "green": { + "label": "Verde", + "description": "Imposta il livello verde del LED. I valori sono 0-255" + }, + "blue": { + "label": "Blu", + "description": "Imposta il livello blu del LED. I valori sono 0-255" + } + }, + "audio": { + "title": "Impostazioni audio", + "description": "Impostazioni per il modulo audio", + "codec2Enabled": { + "label": "Codec 2 Abilitato", + "description": "Abilita la codifica audio Codec 2" + }, + "pttPin": { + "label": "Pin PTT", + "description": "GPIO pin da utilizzare per PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate da usare per la codifica audio" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin da usare per i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin da usare per i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin da usare per i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin da usare per i2S SCK" + } + }, + "cannedMessage": { + "title": "Impostazioni Messaggi Predefiniti", + "description": "Impostazioni per i Messaggi Predefiniti", + "moduleEnabled": { + "label": "Modulo abilitato", + "description": "Abilita Messaggi Predefiniti" + }, + "rotary1Enabled": { + "label": "Encoder Rotativo #1 Abilitato", + "description": "Abilita l'encoder rotativo" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "Valore Pin GPIO (1-39) Per porta encoder A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "Valore Pin GPIO (1-39) Per porta encoder B" + }, + "inputbrokerPinPress": { + "label": "Pin Pressione Encoder", + "description": "Valore Pin GPIO (1-39) Per la pressione dell'encoder" + }, + "inputbrokerEventCw": { + "label": "Evento in senso orario", + "description": "Seleziona evento in ingresso." + }, + "inputbrokerEventCcw": { + "label": "Evento in senso antiorario", + "description": "Seleziona evento in ingresso." + }, + "inputbrokerEventPress": { + "label": "Evento pressione", + "description": "Seleziona evento in ingresso" + }, + "updown1Enabled": { + "label": "Su Giù abilitato", + "description": "Abilita l'encoder su / giù" + }, + "allowInputSource": { + "label": "Consenti sorgente di input", + "description": "Seleziona da: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Invia Campanella", + "description": "Invia un carattere campanella con ogni messaggio" + } + }, + "detectionSensor": { + "title": "Configurazione Sensore Rilevamento", + "description": "Impostazioni per il modulo sensore di rilevamento", + "enabled": { + "label": "Abilitato", + "description": "Abilita o disabilita il modulo del sensore di rilevamento" + }, + "minimumBroadcastSecs": { + "label": "Secondi Di Trasmissione Minimi", + "description": "L'intervallo in secondi di quanto spesso possiamo inviare un messaggio alla mesh quando viene rilevato un cambiamento di stato" + }, + "stateBroadcastSecs": { + "label": "Secondi Trasmissione di Stato", + "description": "L'intervallo in secondi di quanto spesso dovremmo inviare un messaggio alla mesh con lo stato attuale indipendentemente dai cambiamenti" + }, + "sendBell": { + "label": "Invia Campanella", + "description": "Invia campanella ASCII con messaggio di avviso" + }, + "name": { + "label": "Nome Descrittivo", + "description": "Usato per formattare il messaggio inviato a mesh, max 20 Caratteri" + }, + "monitorPin": { + "label": "Pin Monitor", + "description": "Il pin GPIO da monitorare per i cambiamenti di stato" + }, + "detectionTriggerType": { + "label": "Tipo di trigger di rilevamento", + "description": "Il tipo di evento di trigger da usare" + }, + "usePullup": { + "label": "Usa Pullup", + "description": "Indica se usare o meno la modalità INPUT_PULLUP per il pin GPIO" + } + }, + "externalNotification": { + "title": "Configurazione Notifiche Esterne", + "description": "Configura il modulo notifiche esterno", + "enabled": { + "label": "Modulo abilitato", + "description": "Abilita Notifiche Esterne" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Vibrazione Output", + "description": "Vibrazione Output" + }, + "outputBuzzer": { + "label": "Buzzer Output", + "description": "Buzzer Output" + }, + "active": { + "label": "Attivo", + "description": "Attivo" + }, + "alertMessage": { + "label": "Messaggio di allerta", + "description": "Messaggio di allerta" + }, + "alertMessageVibra": { + "label": "Vibrazione Messaggio Di Allerta", + "description": "Vibrazione Messaggio Di Allerta" + }, + "alertMessageBuzzer": { + "label": "Buzzer Messaggio Di Allerta", + "description": "Buzzer Messaggio Di Allerta" + }, + "alertBell": { + "label": "Campanella Di Allarme", + "description": "Occorre attivare una segnalazione quando si riceve un campanello in entrata?" + }, + "alertBellVibra": { + "label": "Vibrazione campanella di allarme", + "description": "Vibrazione campanella di allarme" + }, + "alertBellBuzzer": { + "label": "Buzzer campanella di allarme", + "description": "Buzzer campanella di allarme" + }, + "usePwm": { + "label": "Usa PWM", + "description": "Usa PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Usa I2S come buzzer", + "description": "Designa il Pin I²S come Uscita Buzzer" + } + }, + "mqtt": { + "title": "Impostazioni MQTT", + "description": "Impostazioni per il modulo MQTT", + "enabled": { + "label": "Abilitato", + "description": "Abilita o disabilita MQTT" + }, + "address": { + "label": "Indirizzo Server MQTT", + "description": "Indirizzo server MQTT da usare per i server predefiniti/personalizzati" + }, + "username": { + "label": "Username MQTT", + "description": "Username MQTT da usare per i server predefiniti/personalizzati" + }, + "password": { + "label": "Password MQTT", + "description": "Password MQTT da usare per server predefiniti/personalizzati" + }, + "encryptionEnabled": { + "label": "Crittografia Abilitata", + "description": "Abilita o disabilita la crittografia MQTT. Nota: Tutti i messaggi sono inviati al broker MQTT non cifrati se questa opzione non è abilitata, anche quando i canali uplink hanno chiavi di crittografia impostate. Ciò include i dati di posizione." + }, + "jsonEnabled": { + "label": "JSON Abilitato", + "description": "Indica se inviare/consumare pacchetti JSON su MQTT" + }, + "tlsEnabled": { + "label": "TLS abilitato", + "description": "Abilita o disabilita MQTT" + }, + "root": { + "label": "Root topic", + "description": "Topic root MQTT da utilizzare per server predefiniti/personalizzati" + }, + "proxyToClientEnabled": { + "label": "Proxy Client MQTT Abilitato", + "description": "Utilizza la connessione di rete per fare da proxy ai messaggi MQTT verso il client." + }, + "mapReportingEnabled": { + "label": "Segnalazione sulla Mappa Abilitata", + "description": "Il tuo nodo invierà periodicamente un pacchetto di segnalazione mappa non criptato al server MQTT configurato, questo include id, nome breve e lungo, posizione approssimativa, modello hardware, ruolo, versione del firmware, regione LoRa, preset del modem e nome del canale primario." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Intervallo di Pubblicazione Segnalazione Mappa (s)", + "description": "Intervallo in secondi per pubblicare le segnalazioni mappa" + }, + "positionPrecision": { + "label": "Posizione Approssimativa", + "description": "La posizione condivisa sarà accurata entro questa distanza", + "options": { + "metric_km23": "Entro 23 km", + "metric_km12": "Entro 12 km", + "metric_km5_8": "Entro 5,8 km", + "metric_km2_9": "Entro 2,9 km", + "metric_km1_5": "Entro 1,5 km", + "metric_m700": "Entro 700 m", + "metric_m350": "Entro 350 m", + "metric_m200": "Entro 200 m", + "metric_m90": "Entro 90 m", + "metric_m50": "Entro 50 m", + "imperial_mi15": "Entro 15 miglia", + "imperial_mi7_3": "Entro 7,3 miglia", + "imperial_mi3_6": "Entro 3,6 miglia", + "imperial_mi1_8": "Entro 1,8 miglia", + "imperial_mi0_9": "Entro 0,9 miglia", + "imperial_mi0_5": "Entro 0,5 miglia", + "imperial_mi0_2": "Entro 0,2 miglia", + "imperial_ft600": "Entro 600 piedi", + "imperial_ft300": "Entro 300 piedi", + "imperial_ft150": "Entro 150 piedi" + } + } + } + }, + "neighborInfo": { + "title": "Impostazioni Informazioni Vicini", + "description": "Impostazioni per il modulo Informazioni Vicini", + "enabled": { + "label": "Abilitato", + "description": "Abilita o disabilita Modulo Info Vicini" + }, + "updateInterval": { + "label": "Intervallo di Aggiornamento", + "description": "Intervallo in secondi di quanto spesso dovremmo cercare di inviare le nostre Info Vicini alla mesh" + } + }, + "paxcounter": { + "title": "Impostazioni Paxcounter", + "description": "Impostazioni per il modulo Paxcounter", + "enabled": { + "label": "Modulo abilitato", + "description": "Abilita Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Intervallo di aggiornamento (secondi)", + "description": "Per quanto tempo attendere tra l'invio di pacchetti paxcounter" + }, + "wifiThreshold": { + "label": "Soglia RSSI WiFi", + "description": "A quale livello RSSI WiFi dovrebbe aumentare il contatore. Predefinito a -80." + }, + "bleThreshold": { + "label": "Soglia RSSI BLE", + "description": "A quale livello RSSI BLE dovrebbe aumentare il contatore. Predefinito a -80." + } + }, + "rangeTest": { + "title": "Impostazioni Di Range Test", + "description": "Impostazioni per il modulo Range Test", + "enabled": { + "label": "Modulo abilitato", + "description": "Abilita Range Test" + }, + "sender": { + "label": "Intervallo Dei Messaggi", + "description": "Per quanto tempo attendere tra l'invio dei pacchetti di test" + }, + "save": { + "label": "Salva CSV nella memoria", + "description": "Solo ESP32" + } + }, + "serial": { + "title": "Impostazioni Seriale", + "description": "Impostazioni per il modulo seriale", + "enabled": { + "label": "Modulo abilitato", + "description": "Abilita output seriale" + }, + "echo": { + "label": "Echo", + "description": "Tutti i pacchetti che invii verranno rimandati indietro al tuo dispositivo" + }, + "rxd": { + "label": "Pin di ricezione", + "description": "Imposta il pin GPIO al pin RXD che hai configurato." + }, + "txd": { + "label": "Pin di trasmissione", + "description": "Imposta il pin GPIO al pin TXD che hai configurato." + }, + "baud": { + "label": "Baud rate", + "description": "La velocità di trasmissione seriale" + }, + "timeout": { + "label": "Timeout", + "description": "Secondi da attesa prima di considerare il tuo pacchetto come 'fatto'" + }, + "mode": { + "label": "Modalità", + "description": "Seleziona modalità" + }, + "overrideConsoleSerialPort": { + "label": "Sovrascrivi La Porta Seriale Della Console", + "description": "Se si dispone di una porta seriale collegata alla console, questa verrà sostituita." + } + }, + "storeForward": { + "title": "Configurazione Salva & Inoltra", + "description": "Impostazioni per il modulo Salva & Inoltra", + "enabled": { + "label": "Modulo abilitato", + "description": "Abilita Salva & Inoltra" + }, + "heartbeat": { + "label": "Heartbeat Abilitato", + "description": "Abilita heartbeat Store & Forward" + }, + "records": { + "label": "Numero di record", + "description": "Numero di record da memorizzare" + }, + "historyReturnMax": { + "label": "Cronologia ritorno max", + "description": "Numero massimo di record da restituire" + }, + "historyReturnWindow": { + "label": "Finestra di ritorno cronologia", + "description": "Numero massimo di record da restituire" + } + }, + "telemetry": { + "title": "Impostazioni Telemetria", + "description": "Impostazioni per il modulo Telemetria", + "deviceUpdateInterval": { + "label": "Metriche Dispositivo", + "description": "Intervallo aggiornamento metriche dispositivo (secondi)" + }, + "environmentUpdateInterval": { + "label": "Intervallo aggiornamento metriche ambientali (secondi)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Modulo abilitato", + "description": "Abilita la telemetria Ambiente" + }, + "environmentScreenEnabled": { + "label": "Visualizzato sullo schermo", + "description": "Mostra il modulo di telemetria sull'OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Mostra Fahrenheit", + "description": "Mostra la temperatura in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Qualità Dell'Aria Abilitata", + "description": "Abilita la telemetria della qualità dell'aria" + }, + "airQualityInterval": { + "label": "Intervallo Di Aggiornamento Qualità Dell'Aria", + "description": "Quanto spesso inviare dati sulla qualità dell'aria sulla mesh" + }, + "powerMeasurementEnabled": { + "label": "Misurazione Alimentazione Abilitata", + "description": "Abilita la telemetria di misura della alimentazione" + }, + "powerUpdateInterval": { + "label": "Intervallo Di Aggiornamento Alimentazione", + "description": "Quanto spesso inviare i dati di alimentazione sulla mesh" + }, + "powerScreenEnabled": { + "label": "Schermo Di Alimentazione Abilitato", + "description": "Abilita lo schermo di Telemetria di alimentazione" + } + } +} diff --git a/packages/web/public/i18n/locales/it-IT/nodes.json b/packages/web/public/i18n/locales/it-IT/nodes.json new file mode 100644 index 00000000..08bb7559 --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Chiave Pubblica Abilitata" + }, + "noPublicKey": { + "label": "Nessuna Chiave Pubblica" + }, + "directMessage": { + "label": "Messaggio Diretto {{shortName}}" + }, + "favorite": { + "label": "Preferito", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Non è un preferito" + }, + "error": { + "label": "Errori", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Sentito", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Altezza" + }, + "channelUtil": { + "label": "Utilizzo Canale" + }, + "airtimeUtil": { + "label": "Utilizzo Airtime" + } + }, + "nodesTable": { + "headings": { + "longName": "Nome Lungo", + "connection": "Connessione", + "lastHeard": "Ultimo Contatto", + "encryption": "Crittografia", + "model": "Modello", + "macAddress": "Indirizzo MAC" + }, + "connectionStatus": { + "direct": "Diretto", + "away": "assente", + "unknown": "-", + "viaMqtt": ", tramite MQTT" + }, + "lastHeardStatus": { + "never": "Mai" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Richiedi posizione" + } +} diff --git a/packages/web/public/i18n/locales/it-IT/ui.json b/packages/web/public/i18n/locales/it-IT/ui.json new file mode 100644 index 00000000..2925be76 --- /dev/null +++ b/packages/web/public/i18n/locales/it-IT/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigazione", + "messages": "Messaggi", + "map": "Mappa", + "config": "Configurazione", + "radioConfig": "Configurazione Radio", + "moduleConfig": "Configurazione Modulo", + "channels": "Canali", + "nodes": "Nodi" + }, + "app": { + "title": "Meshtastic", + "logo": "Logo Meshtastic" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Apri barra laterale", + "close": "Chiudi barra laterale" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volt", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Data di build: {{date}}" + }, + "deviceName": { + "title": "Nome del dispositivo", + "changeName": "Cambia Nome Del Dispositivo", + "placeholder": "Inserisci il nome del dispositivo" + }, + "editDeviceName": "Modifica il nome del dispositivo" + } + }, + "batteryStatus": { + "charging": "{{level}}% in carica", + "pluggedIn": "Alimentato", + "title": "Batteria" + }, + "search": { + "nodes": "Cerca nodi...", + "channels": "Cerca canali...", + "commandPalette": "Cerca comandi..." + }, + "toast": { + "positionRequestSent": { + "title": "Richiesta di posizione inviata." + }, + "requestingPosition": { + "title": "Richiesta di posizione, attendere prego..." + }, + "sendingTraceroute": { + "title": "Invio di Traceroute, attendere prego..." + }, + "tracerouteSent": { + "title": "Traceroute inviato." + }, + "savedChannel": { + "title": "Canale Salvato: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "La chat sta usando la crittografia PKI." + }, + "pskEncryption": { + "title": "La chat sta usando la crittografia PSK." + } + }, + "configSaveError": { + "title": "Errore Salvataggio Configurazione", + "description": "Si è verificato un errore durante il salvataggio di questa configurazione." + }, + "validationError": { + "title": "Esistono errori di configurazione", + "description": "Correggi gli errori di configurazione prima di salvare." + }, + "saveSuccess": { + "title": "Salvataggio Configurazione", + "description": "La modifica di configurazione {{case}} è stata salvata." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copiato!" + }, + "copyToClipboard": { + "label": "Copia negli appunti" + }, + "hidePassword": { + "label": "Nascondi password" + }, + "showPassword": { + "label": "Visualizza password" + }, + "deliveryStatus": { + "delivered": "Consegnato", + "failed": "Consegna Fallita", + "waiting": "In attesa", + "unknown": "Sconosciuto" + } + }, + "general": { + "label": "Generale" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Statistiche" + }, + "role": { + "label": "Ruolo" + }, + "filter": { + "label": "Filtro" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Cancella input" + }, + "resetFilters": { + "label": "Ripristina Filtri" + }, + "nodeName": { + "label": "Nome/numero del nodo", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Tempo di Trasmissione Utilizzato (%)" + }, + "batteryLevel": { + "label": "Livello batteria (%)", + "labelText": "Livello batteria (%): {{value}}" + }, + "batteryVoltage": { + "label": "Tensione della Batteria (V)", + "title": "Tensione" + }, + "channelUtilization": { + "label": "Utilizzo Canale (%)" + }, + "hops": { + "direct": "Diretto", + "label": "Numero di hop", + "text": "Numero di hop: {{value}}" + }, + "lastHeard": { + "label": "Ricevuto più di recente", + "labelText": "Ultimo contatto: {{value}}", + "nowLabel": "Adesso" + }, + "snr": { + "label": "SNR (dB)" + }, + "favorites": { + "label": "Preferiti" + }, + "hide": { + "label": "Nascondi" + }, + "showOnly": { + "label": "Mostra Solo" + }, + "viaMqtt": { + "label": "Connesso tramite MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Lingua", + "changeLanguage": "Cambia Lingua" + }, + "theme": { + "dark": "Scuro", + "light": "Chiaro", + "system": "Automatico", + "changeTheme": "Modifica lo schema dei colori" + }, + "errorPage": { + "title": "Questo è un po' imbarazzante...", + "description1": "Siamo davvero spiacenti, ma si è verificato un errore nel client web che ha causato il crash.
Questo non dovrebbe accadere e stiamo lavorando duramente per risolverlo.", + "description2": "Il modo migliore per evitare che ciò accada di nuovo a voi o a chiunque altro è quello di riferire la questione a noi.", + "reportInstructions": "Per favore includi le seguenti informazioni nel tuo report:", + "reportSteps": { + "step1": "Cosa stavi facendo quando si è verificato l'errore", + "step2": "Cosa ti aspettavi succedesse", + "step3": "Quello che è successo realmente", + "step4": "Altre informazioni rilevanti" + }, + "reportLink": "Puoi segnalare il problema al nostro <0>GitHub", + "dashboardLink": "Ritorna alla <0>dashboard", + "detailsSummary": "Dettagli Errore", + "errorMessageLabel": "Messaggio di errore:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® è un marchio registrato di Meshtastic LLC. | <1>Informazioni Legali", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/channels.json b/packages/web/public/i18n/locales/ja-JP/channels.json new file mode 100644 index 00000000..5dc28596 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "チャンネル", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "プライマリ", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "チャンネル設定", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "役割", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "名前", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/commandPalette.json b/packages/web/public/i18n/locales/ja-JP/commandPalette.json new file mode 100644 index 00000000..d43e0ee4 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "メッセージ", + "map": "地図", + "config": "Config", + "channels": "チャンネル", + "nodes": "ノード" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "インポート", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/common.json b/packages/web/public/i18n/locales/ja-JP/common.json new file mode 100644 index 00000000..000754fb --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "適用", + "backupKey": "Backup Key", + "cancel": "キャンセル", + "clearMessages": "Clear Messages", + "close": "終了", + "confirm": "Confirm", + "delete": "削除", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "インポート", + "message": "メッセージ", + "now": "Now", + "ok": "OK", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "削除", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "リセット", + "save": "保存", + "scanQr": "QRコードをスキャン", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SN比", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/dashboard.json b/packages/web/public/i18n/locales/ja-JP/dashboard.json new file mode 100644 index 00000000..890b4644 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "シリアル", + "connectionType_network": "ネットワーク", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/deviceConfig.json b/packages/web/public/i18n/locales/ja-JP/deviceConfig.json new file mode 100644 index 00000000..eaaa6010 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "接続するデバイスを選択", + "tabDisplay": "表示", + "tabLora": "LoRa", + "tabNetwork": "ネットワーク", + "tabPosition": "位置", + "tabPower": "電源", + "tabSecurity": "セキュリティ" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX 時間帯" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "役割" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Enabled" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "ペアリングモード" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "帯域" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "MQTT を無視" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "モデムプリセット" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "MQTTを許可" + }, + "overrideDutyCycle": { + "description": "デューティサイクルを上書き", + "label": "デューティサイクルを上書き" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "リージョン" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Enabled" + }, + "gateway": { + "description": "Default Gateway", + "label": "ゲートウェイ" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "サブネット" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Enabled" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "UDP Config" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "タイムスタンプ", + "unset": "削除", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "省電力モードを有効化" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "電源設定" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "秘密鍵" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "公開鍵" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/dialog.json b/packages/web/public/i18n/locales/ja-JP/dialog.json new file mode 100644 index 00000000..86498524 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "シリアル", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Connect", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "メッセージ", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "電圧", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generate QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "よろしいですか?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "よろしいですか?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/messages.json b/packages/web/public/i18n/locales/ja-JP/messages.json new file mode 100644 index 00000000..62ae8df7 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "送信" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "返信" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/moduleConfig.json b/packages/web/public/i18n/locales/ja-JP/moduleConfig.json new file mode 100644 index 00000000..c6d82c85 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "環境照明", + "tabAudio": "オーディオ", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "検出センサー", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "隣接ノード情報", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "レンジテスト", + "tabSerial": "シリアル", + "tabStoreAndForward": "S&F", + "tabTelemetry": "テレメトリー" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "電流", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "赤", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "緑", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "青", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "ルート トピック", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "タイムアウト", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "サーバーの最大保管レコード数 (デフォルト 約11,000レコード)", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "リクエスト可能な最大の履歴件数", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "リクエスト可能な履歴の期間 (分)", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "デバイスのメトリック更新間隔 (秒)" + }, + "environmentUpdateInterval": { + "label": "環境メトリック更新間隔 (秒)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/nodes.json b/packages/web/public/i18n/locales/ja-JP/nodes.json new file mode 100644 index 00000000..181220f0 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "お気に入り", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "エラー", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "直接", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/ja-JP/ui.json b/packages/web/public/i18n/locales/ja-JP/ui.json new file mode 100644 index 00000000..56983270 --- /dev/null +++ b/packages/web/public/i18n/locales/ja-JP/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "メッセージ", + "map": "地図", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "チャンネル", + "nodes": "ノード" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "ファームウェア", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "バッテリー" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "パスワードを非表示" + }, + "showPassword": { + "label": "パスワードを表示" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "ハードウェア" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "役割" + }, + "filter": { + "label": "絞り込み" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "電圧" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "直接", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "最後の通信", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "言語設定", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "ダーク", + "light": "ライト", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/channels.json b/packages/web/public/i18n/locales/ko-KR/channels.json new file mode 100644 index 00000000..0987f269 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "채널", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Primary", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "채널 설정", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "역할", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "이름", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/commandPalette.json b/packages/web/public/i18n/locales/ko-KR/commandPalette.json new file mode 100644 index 00000000..a7bcc191 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "메시지", + "map": "지도", + "config": "Config", + "channels": "채널", + "nodes": "Nodes" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "불러오기", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/common.json b/packages/web/public/i18n/locales/ko-KR/common.json new file mode 100644 index 00000000..6b7b6a87 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "적용", + "backupKey": "Backup Key", + "cancel": "취소", + "clearMessages": "Clear Messages", + "close": "닫기", + "confirm": "Confirm", + "delete": "삭제", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "불러오기", + "message": "메시지", + "now": "Now", + "ok": "확인", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "지우기", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "초기화", + "save": "저장", + "scanQr": " QR코드 스캔", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/dashboard.json b/packages/web/public/i18n/locales/ko-KR/dashboard.json new file mode 100644 index 00000000..c3dc1447 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "시리얼", + "connectionType_network": "네트워크", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/deviceConfig.json b/packages/web/public/i18n/locales/ko-KR/deviceConfig.json new file mode 100644 index 00000000..c26b0244 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "블루투스", + "tabDevice": "기기", + "tabDisplay": "화면", + "tabLora": "LoRa", + "tabNetwork": "네트워크", + "tabPosition": "위치", + "tabPower": "전원", + "tabSecurity": "보안" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX 시간대" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "역할" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Enabled" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "페어링 모드" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "대역폭" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "MQTT로 부터 수신 무시" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "모뎀 프리셋" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "MQTT로 전송 허용" + }, + "overrideDutyCycle": { + "description": "Duty Cycle 무시", + "label": "Duty Cycle 무시" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "지역" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Enabled" + }, + "gateway": { + "description": "Default Gateway", + "label": "게이트웨이" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "서브넷" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Enabled" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "UDP 설정" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "타임스탬프", + "unset": "해제", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "저젼력 모드 설정" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "전원 설정" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "개인 키" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "공개 키" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/dialog.json b/packages/web/public/i18n/locales/ko-KR/dialog.json new file mode 100644 index 00000000..8e2459f4 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "블루투스", + "tabSerial": "시리얼", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Connect", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "메시지", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "전압", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generate QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "확실합니까?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "확실합니까?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/messages.json b/packages/web/public/i18n/locales/ko-KR/messages.json new file mode 100644 index 00000000..e1d652ea --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "보내기" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Reply" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/moduleConfig.json b/packages/web/public/i18n/locales/ko-KR/moduleConfig.json new file mode 100644 index 00000000..628da18b --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "조명", + "tabAudio": "오디오", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "감지 센서", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "이웃 정보", + "tabPaxcounter": "팍스카운터", + "tabRangeTest": "거리 테스트", + "tabSerial": "시리얼", + "tabStoreAndForward": "S&F", + "tabTelemetry": "텔레메트리" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "전류", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "빨강", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "초록", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "파랑", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Root topic", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "시간 초과됨", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Number of records", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "기기 메트릭 업데이트 간격 (초)" + }, + "environmentUpdateInterval": { + "label": "환경 메트릭 업데이트 간격 (초)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/nodes.json b/packages/web/public/i18n/locales/ko-KR/nodes.json new file mode 100644 index 00000000..e6e37463 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "즐겨찾기", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "오류", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "직접 연결", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/ko-KR/ui.json b/packages/web/public/i18n/locales/ko-KR/ui.json new file mode 100644 index 00000000..d413bed1 --- /dev/null +++ b/packages/web/public/i18n/locales/ko-KR/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "메시지", + "map": "지도", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "채널", + "nodes": "Nodes" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "펌웨어", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "배터리" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "비밀번호 숨김" + }, + "showPassword": { + "label": "비밀번호 보기" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "하드웨어" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "역할" + }, + "filter": { + "label": "필터" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "전압" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "직접 연결", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "최근 수신", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "언어", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "다크", + "light": "라이트", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/channels.json b/packages/web/public/i18n/locales/nl-NL/channels.json new file mode 100644 index 00000000..ef9b4d4b --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Kanalen", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Primair", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Kanaalinstellingen", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Functie", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "Naam", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/commandPalette.json b/packages/web/public/i18n/locales/nl-NL/commandPalette.json new file mode 100644 index 00000000..87c47c87 --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Berichten", + "map": "Kaart", + "config": "Config", + "channels": "Kanalen", + "nodes": "Nodes" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "Importeer", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/common.json b/packages/web/public/i18n/locales/nl-NL/common.json new file mode 100644 index 00000000..e426f48b --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Toepassen", + "backupKey": "Backup Key", + "cancel": "Annuleer", + "clearMessages": "Clear Messages", + "close": "Sluit", + "confirm": "Confirm", + "delete": "Verwijder", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "Importeer", + "message": "Bericht", + "now": "Now", + "ok": "OK", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Verwijder", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Reset", + "save": "Opslaan", + "scanQr": "Scan QR-code", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/dashboard.json b/packages/web/public/i18n/locales/nl-NL/dashboard.json new file mode 100644 index 00000000..3f7c26d6 --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Serieel", + "connectionType_network": "Netwerk", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/deviceConfig.json b/packages/web/public/i18n/locales/nl-NL/deviceConfig.json new file mode 100644 index 00000000..19db7dae --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Apparaat", + "tabDisplay": "Weergave", + "tabLora": "LoRa", + "tabNetwork": "Netwerk", + "tabPosition": "Positie", + "tabPower": "Vermogen", + "tabSecurity": "Beveiliging" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX Timezone" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Functie" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Enabled" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Koppelmodus" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Bandbreedte" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Negeer MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Modem Preset" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "OK to MQTT" + }, + "overrideDutyCycle": { + "description": "Overschrijf Duty Cycle", + "label": "Overschrijf Duty Cycle" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Regio" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Enabled" + }, + "gateway": { + "description": "Default Gateway", + "label": "Gateway" + }, + "ip": { + "description": "IP Address", + "label": "IP-adres" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Subnet" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Enabled" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "UDP Configuratie" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Tijdstempel", + "unset": "Terugzetten", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Energiebesparingsmodus inschakelen" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Energie configuratie" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Privésleutel" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Publieke sleutel" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/dialog.json b/packages/web/public/i18n/locales/nl-NL/dialog.json new file mode 100644 index 00000000..0c6555cd --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Serieel", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Verbinding maken", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Bericht", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Spanning", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generate QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Weet u het zeker?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "Weet u het zeker?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/messages.json b/packages/web/public/i18n/locales/nl-NL/messages.json new file mode 100644 index 00000000..2572ee9e --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Verzend" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Reply" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/moduleConfig.json b/packages/web/public/i18n/locales/nl-NL/moduleConfig.json new file mode 100644 index 00000000..2906dd8b --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Sfeerverlichting", + "tabAudio": "Geluid", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Detectie Sensor", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Neighbor Info", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Bereik Test", + "tabSerial": "Serieel", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Telemetrie" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Huidige", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Rood", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Groen", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Blauw", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Root topic", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Time-Out", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Aantal records", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Device metrics update interval (seconds)" + }, + "environmentUpdateInterval": { + "label": "Environment metrics update interval (seconds)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/nodes.json b/packages/web/public/i18n/locales/nl-NL/nodes.json new file mode 100644 index 00000000..925ab911 --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Favoriet", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Foutmelding", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "Direct", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/nl-NL/ui.json b/packages/web/public/i18n/locales/nl-NL/ui.json new file mode 100644 index 00000000..2cb34f46 --- /dev/null +++ b/packages/web/public/i18n/locales/nl-NL/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Berichten", + "map": "Kaart", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "Kanalen", + "nodes": "Nodes" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Batterij" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Wachtwoord verbergen" + }, + "showPassword": { + "label": "Wachtwoord tonen" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Functie" + }, + "filter": { + "label": "Filter" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Spanning" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Direct", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Laatst gehoord", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Taal", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "Donker", + "light": "Licht", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/channels.json b/packages/web/public/i18n/locales/pl-PL/channels.json new file mode 100644 index 00000000..ddbc26b5 --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Kanały", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Podstawowy", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Ustawienia kanału", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Rola", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "Nazwa", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/commandPalette.json b/packages/web/public/i18n/locales/pl-PL/commandPalette.json new file mode 100644 index 00000000..5526ed2d --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Wiadomości", + "map": "Mapa", + "config": "Config", + "channels": "Kanały", + "nodes": "Nodes" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "Import", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/common.json b/packages/web/public/i18n/locales/pl-PL/common.json new file mode 100644 index 00000000..6aa8233a --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Zastosuj", + "backupKey": "Backup Key", + "cancel": "Anuluj", + "clearMessages": "Clear Messages", + "close": "Zamknij", + "confirm": "Confirm", + "delete": "Usuń", + "dismiss": "Zamknij", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "Import", + "message": "Wiadomość", + "now": "Now", + "ok": "OK", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Usuń", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Zresetuj", + "save": "Zapisz", + "scanQr": "Scan QR Code", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR:", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Nieznany", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/dashboard.json b/packages/web/public/i18n/locales/pl-PL/dashboard.json new file mode 100644 index 00000000..f479246f --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Seryjny", + "connectionType_network": "Sieć", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/deviceConfig.json b/packages/web/public/i18n/locales/pl-PL/deviceConfig.json new file mode 100644 index 00000000..406284fe --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Urządzenie", + "tabDisplay": "Wyświetlacz", + "tabLora": "LoRa", + "tabNetwork": "Sieć", + "tabPosition": "Pozycjonowanie", + "tabPower": "Zasilanie", + "tabSecurity": "Bezpieczeństwo" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX Timezone" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Rola" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Włączony" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Pairing mode" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Bandwidth" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Ignore MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Modem Preset" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "OK to MQTT" + }, + "overrideDutyCycle": { + "description": "Override Duty Cycle", + "label": "Override Duty Cycle" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Region" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Ustawienia radia", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Włączony" + }, + "gateway": { + "description": "Default Gateway", + "label": "Brama domyślna" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Subnet" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Włączony" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "Ustawienia UDP" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Znacznik czasu", + "unset": "Nieustawiony", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Enable power saving mode" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Konfiguracja zarządzania energią" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Klucz prywatny" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Klucz publiczny" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/dialog.json b/packages/web/public/i18n/locales/pl-PL/dialog.json new file mode 100644 index 00000000..bf55cb58 --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Seryjny", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Connect", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Wiadomość", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Napięcie", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generuj Kod QR" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Jesteś pewny?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "Jesteś pewny?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/messages.json b/packages/web/public/i18n/locales/pl-PL/messages.json new file mode 100644 index 00000000..c947bf7e --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Wyślij" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Odpowiedz" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/moduleConfig.json b/packages/web/public/i18n/locales/pl-PL/moduleConfig.json new file mode 100644 index 00000000..66d635ef --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Ambient Lighting", + "tabAudio": "Audio", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Detection Sensor", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Neighbor Info", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Test zasięgu", + "tabSerial": "Seryjny", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Telemetria" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Natężenie", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Red", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Green", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Blue", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Włączony", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Włączony", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Root topic", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Włączony", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Limit czasu", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Tryb", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Number of records", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Device metrics update interval (seconds)" + }, + "environmentUpdateInterval": { + "label": "Environment metrics update interval (seconds)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/nodes.json b/packages/web/public/i18n/locales/pl-PL/nodes.json new file mode 100644 index 00000000..7359999d --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Ulubiony", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Błąd", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Usłyszano", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "Bezpośrednio", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/pl-PL/ui.json b/packages/web/public/i18n/locales/pl-PL/ui.json new file mode 100644 index 00000000..5be83c39 --- /dev/null +++ b/packages/web/public/i18n/locales/pl-PL/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Wiadomości", + "map": "Mapa", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "Kanały", + "nodes": "Nodes" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Bateria" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Ukryj hasło" + }, + "showPassword": { + "label": "Pokaż hasło" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Czekam. . .", + "unknown": "Nieznany" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Rola" + }, + "filter": { + "label": "Filtr" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Napięcie" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Bezpośrednio", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Aktywność", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Język", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "Ciemny", + "light": "Jasny", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/channels.json b/packages/web/public/i18n/locales/pt-PT/channels.json new file mode 100644 index 00000000..ab1e9169 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Canal", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Principal", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Configurações de canal", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Papel", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "Nome", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/commandPalette.json b/packages/web/public/i18n/locales/pt-PT/commandPalette.json new file mode 100644 index 00000000..6835f1a5 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Mensagens", + "map": "Mapa", + "config": "Config", + "channels": "Canal", + "nodes": "Nodes" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "Importar", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Depuração", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/common.json b/packages/web/public/i18n/locales/pt-PT/common.json new file mode 100644 index 00000000..c2d1d592 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Aplicar", + "backupKey": "Backup Key", + "cancel": "Cancelar", + "clearMessages": "Clear Messages", + "close": "Fechar", + "confirm": "Confirm", + "delete": "Excluir", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "Importar", + "message": "Mensagem", + "now": "Now", + "ok": "Okay", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Remover", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Redefinir", + "save": "Salvar", + "scanQr": "Ler código QR", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/dashboard.json b/packages/web/public/i18n/locales/pt-PT/dashboard.json new file mode 100644 index 00000000..b68c6190 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Série", + "connectionType_network": "Rede", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/deviceConfig.json b/packages/web/public/i18n/locales/pt-PT/deviceConfig.json new file mode 100644 index 00000000..091a01c8 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Dispositivo", + "tabDisplay": "Ecrã", + "tabLora": "LoRa", + "tabNetwork": "Rede", + "tabPosition": "Posição", + "tabPower": "Energia", + "tabSecurity": "Segurança" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "Fuso horário POSIX" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Papel" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Enabled" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Modo de emparelhamento" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Largura de banda" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Ignorar MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Predefinição de modem" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "Disponibilizar no MQTT" + }, + "overrideDutyCycle": { + "description": "Ignorar ciclo de trabalho", + "label": "Ignorar ciclo de trabalho" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Região" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Enabled" + }, + "gateway": { + "description": "Default Gateway", + "label": "Gateway" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Subnet" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Enabled" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "Configuração UDP" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Data e hora", + "unset": "Não Definido", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Ativar modo de poupança de energia" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Configuração de Energia" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Chave privada" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Chave pública" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/dialog.json b/packages/web/public/i18n/locales/pt-PT/dialog.json new file mode 100644 index 00000000..a584527b --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Série", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Ligar", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Mensagem", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Voltagem", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generate QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Confirma?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "Confirma?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/messages.json b/packages/web/public/i18n/locales/pt-PT/messages.json new file mode 100644 index 00000000..cd7260a8 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Enviar" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Responder" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/moduleConfig.json b/packages/web/public/i18n/locales/pt-PT/moduleConfig.json new file mode 100644 index 00000000..e3a638f8 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Iluminação ambiente", + "tabAudio": "Áudio", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Sensor de deteção", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Informações da vizinhança", + "tabPaxcounter": "Contador de pessoas", + "tabRangeTest": "Teste de Alcance", + "tabSerial": "Série", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Telemetria" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Atual", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Vermelho", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Verde", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Azul", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Tópico principal", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Timeout", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Número de registos", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Intervalo de atualização de métricas do dispositivo (segundos)" + }, + "environmentUpdateInterval": { + "label": "Intervalo de atualização de métricas de ambiente (segundos)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/nodes.json b/packages/web/public/i18n/locales/pt-PT/nodes.json new file mode 100644 index 00000000..f263d4cc --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Favoritos", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Erros", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "Direto", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/pt-PT/ui.json b/packages/web/public/i18n/locales/pt-PT/ui.json new file mode 100644 index 00000000..6e413306 --- /dev/null +++ b/packages/web/public/i18n/locales/pt-PT/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Mensagens", + "map": "Mapa", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "Canal", + "nodes": "Nodes" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Bateria" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Ocultar palavra-passe" + }, + "showPassword": { + "label": "Mostrar palavra-passe" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Papel" + }, + "filter": { + "label": "Filtrar" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Voltagem" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Direto", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Último recebido", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Idioma", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "Escuro", + "light": "Claro", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/channels.json b/packages/web/public/i18n/locales/sv-SE/channels.json new file mode 100644 index 00000000..de35fc36 --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Kanaler", + "channelName": "Kanal: {channelName}", + "broadcastLabel": "Primär", + "channelIndex": "# {{index}}" + }, + "validation": { + "pskInvalid": "Ange en giltig {{bits}}-bitars PSK." + }, + "settings": { + "label": "Kanalinställningar", + "description": "Krypto, MQTT & diverse inställningar" + }, + "role": { + "label": "Roll", + "description": "Enhetens telemetri skickas över PRIMÄR. Endast en PRIMÄR kanal tillåts", + "options": { + "primary": "PRIMÄR", + "disabled": "INAKTIVERAD", + "secondary": "SEKUNDÄR" + } + }, + "psk": { + "label": "Delad nyckel (PSK)", + "description": "PSK-längder som stöds: 256-bit, 128-bit, 8-bit eller tom (0-bit)", + "generate": "Generera" + }, + "name": { + "label": "Namn", + "description": "Ett unikt namn för kanalen, <12 bytes. Lämna tomt för standardnamn" + }, + "uplinkEnabled": { + "label": "Upplänk aktiverad", + "description": "Skicka meddelanden från det lokala nätet till MQTT" + }, + "downlinkEnabled": { + "label": "Nedlänk aktiverad", + "description": "Skicka meddelanden från MQTT till det lokala nätet" + }, + "positionPrecision": { + "label": "Plats", + "description": "Platsprecisionen som kan delas med kanalen. Kan inaktiveras.", + "options": { + "none": "Dela inte plats", + "precise": "Exakt plats", + "metric_km23": "Inom 23 kilometer", + "metric_km12": "Inom 12 kilometer", + "metric_km5_8": "Inom 5,8 kilometer", + "metric_km2_9": "Inom 2,9 kilometer", + "metric_km1_5": "Inom 1,5 kilometer", + "metric_m700": "Inom 700 meter", + "metric_m350": "Inom 350 meter", + "metric_m200": "Inom 200 meter", + "metric_m90": "Inom 90 meter", + "metric_m50": "Inom 50 meter", + "imperial_mi15": "Inom 15 engelska mil", + "imperial_mi7_3": "Inom 7,3 engelska mil", + "imperial_mi3_6": "Inom 3,6 engelska mil", + "imperial_mi1_8": "Inom 1,8 engelska mil", + "imperial_mi0_9": "Inom 0,9 engelska mil", + "imperial_mi0_5": "Inom 0,5 engelska mil", + "imperial_mi0_2": "Inom 0,2 engelska mil", + "imperial_ft600": "Inom 600 fot", + "imperial_ft300": "Inom 300 fot", + "imperial_ft150": "Inom 150 fot" + } + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/commandPalette.json b/packages/web/public/i18n/locales/sv-SE/commandPalette.json new file mode 100644 index 00000000..08f5814d --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "Inga resultat hittades.", + "page": { + "title": "Snabbmeny" + }, + "pinGroup": { + "label": "Lägg till grupp i favoriter" + }, + "unpinGroup": { + "label": "Ta bort grupp från favoriter" + }, + "goto": { + "label": "Gå till", + "command": { + "messages": "Meddelanden", + "map": "Karta", + "config": "Inställningar", + "channels": "Kanaler", + "nodes": "Noder" + } + }, + "manage": { + "label": "Hantera", + "command": { + "switchNode": "Byt nod", + "connectNewNode": "Anslut ny nod" + } + }, + "contextual": { + "label": "Kontextuell", + "command": { + "qrCode": "QR-kod", + "qrGenerator": "Generator", + "qrImport": "Import", + "scheduleShutdown": "Schemalägg avstängning", + "scheduleReboot": "Schemalägg omstart", + "rebootToOtaMode": "Starta om till OTA-läge", + "resetNodeDb": "Töm noddatabasen", + "factoryResetDevice": "Fabriksåterställ enhet", + "factoryResetConfig": "Fabriksåterställ konfigurationen" + } + }, + "debug": { + "label": "Felsökning", + "command": { + "reconfigure": "Konfigurera om", + "clearAllStoredMessages": "Radera alla sparade meddelanden" + } + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/common.json b/packages/web/public/i18n/locales/sv-SE/common.json new file mode 100644 index 00000000..997064ee --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Verkställ", + "backupKey": "Säkerhetskopiera nyckel", + "cancel": "Avbryt", + "clearMessages": "Ta bort meddelanden", + "close": "Stäng", + "confirm": "Bekräfta", + "delete": "Radera", + "dismiss": "Stäng", + "download": "Ladda ner", + "export": "Exportera", + "generate": "Generera", + "regenerate": "Förnya", + "import": "Importera", + "message": "Meddelande", + "now": "Nu", + "ok": "Okej", + "print": "Skriv ut", + "rebootOtaNow": "Starta om till OTA-läge nu", + "remove": "Ta bort", + "requestNewKeys": "Begär nya nycklar", + "requestPosition": "Begär position", + "reset": "Nollställ", + "save": "Spara", + "scanQr": "Skanna QR-kod", + "traceRoute": "Spåra rutt", + "submit": "Spara" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic webbklient" + }, + "loading": "Laddar...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "hopp", + "plural": "hopp" + }, + "hopsAway": { + "one": "{{count}} hopp bort", + "plural": "{{count}} hopp bort", + "unknown": "Okänt antal hopp bort" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meter", + "suffix": "m" + }, + "minute": { + "one": "minut", + "plural": "minuter" + }, + "hour": { + "one": "Timme", + "plural": "Timmar" + }, + "millisecond": { + "one": "millisekund", + "plural": "millisekunder", + "suffix": "ms" + }, + "second": { + "one": "sekund", + "plural": "sekunder" + }, + "day": { + "one": "Dag", + "plural": "Dagar" + }, + "month": { + "one": "Månad", + "plural": "Månader" + }, + "year": { + "one": "År", + "plural": "År" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volt", + "suffix": "V" + }, + "record": { + "one": "Post", + "plural": "Poster" + } + }, + "security": { + "0bit": "Tom", + "8bit": "8 bitar", + "128bit": "128 bitar", + "256bit": "256 bitar" + }, + "unknown": { + "longName": "Okänd", + "shortName": "UNK", + "notAvailable": "–", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "EJ SATT", + "fallbackName": "Meshtastic {{last4}}", + "node": "Nod", + "formValidation": { + "unsavedChanges": "Osparade ändringar", + "tooBig": { + "string": "Texten är för lång. Ange en text mindre än eller lika med {{maximum}} tecken långt.", + "number": "Värdet är för stort. Ange ett numeriskt värde mindre än eller lika med {{maximum}}.", + "bytes": "Värdet är för stort. En längd mindre än eller lika med {{params.maximum}} bytes krävs." + }, + "tooSmall": { + "string": "Texten är för kort. Ange en text längre än eller lika med {{minimum}} tecken långt.", + "number": "Värdet är för litet. Ange ett numeriskt värde större än eller lika med {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Ogiltigt format, en giltig IPv4-adress krävs.", + "key": "Ogiltigt format, en giltig Base64-kodad nyckel krävs (PSK)." + }, + "invalidType": { + "number": "Ogiltig typ, fältet måste innehålla ett tal." + }, + "pskLength": { + "0bit": "Fältet måste vara tomt.", + "8bit": "Nyckeln måste vara en 8-bitars nyckel (PSK).", + "128bit": "Nyckeln måste vara en 128-bitars nyckel (PSK).", + "256bit": "Nyckeln måste vara en 256-bitars nyckel (PSK)." + }, + "required": { + "generic": "Obligatoriskt fält.", + "managed": "Åtminstone en administratörsnyckel krävs om noden fjärrhanteras.", + "key": "En nyckel måste anges." + } + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/dashboard.json b/packages/web/public/i18n/locales/sv-SE/dashboard.json new file mode 100644 index 00000000..f9ac6a24 --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Anslutna enheter", + "description": "Hantera dina anslutna Meshtastic-enheter.", + "connectionType_ble": "BLE", + "connectionType_serial": "Seriell kommunikation", + "connectionType_network": "Nätverk", + "noDevicesTitle": "Inga anslutna enheter ", + "noDevicesDescription": "Anslut en ny enhet för att komma igång.", + "button_newConnection": "Ny anslutning" + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/deviceConfig.json b/packages/web/public/i18n/locales/sv-SE/deviceConfig.json new file mode 100644 index 00000000..d582dff5 --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Konfiguration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Enhet", + "tabDisplay": "Display", + "tabLora": "LoRa", + "tabNetwork": "Nätverk", + "tabPosition": "Plats", + "tabPower": "Ström", + "tabSecurity": "Säkerhet" + }, + "sidebar": { + "label": "Moduler" + }, + "device": { + "title": "Enhetsinställningar", + "description": "Inställningar för enheten", + "buttonPin": { + "description": "Stift för knapp", + "label": "Knapp-stift" + }, + "buzzerPin": { + "description": "Stift för summer", + "label": "Summer-stift" + }, + "disableTripleClick": { + "description": "Inaktivera trippeltryck", + "label": "Inaktivera trippeltryck" + }, + "doubleTapAsButtonPress": { + "description": "Behandla dubbeltryck som knapptryck", + "label": "Dubbeltryck som knapptryck" + }, + "ledHeartbeatDisabled": { + "description": "Inaktivera blinkande LED", + "label": "LED-puls inaktiverad" + }, + "nodeInfoBroadcastInterval": { + "description": "Hur ofta nod-info ska sändas", + "label": "Sändningsintervall för nod-info" + }, + "posixTimezone": { + "description": "POSIX-tidszonsträng för enheten", + "label": "POSIX-tidszon" + }, + "rebroadcastMode": { + "description": "Hur enheten hanterar återutsändning", + "label": "Återutsändningsläge" + }, + "role": { + "description": "Vilken roll enheten har på nätet", + "label": "Roll" + } + }, + "bluetooth": { + "title": "Bluetooth-inställningar", + "description": "Inställningar för Bluetooth-modulen", + "note": "OBS! Vissa enheter (ESP32) kan inte använda både Bluetooth och WiFi samtidigt.", + "enabled": { + "description": "Aktivera eller inaktivera Bluetooth", + "label": "Aktiverad" + }, + "pairingMode": { + "description": "Metod för parkoppling", + "label": "Parkopplingsläge" + }, + "pin": { + "description": "Pinkod för parkoppling", + "label": "Pinkod" + } + }, + "display": { + "description": "Inställningar för enhetens display", + "title": "Displayinställningar", + "headingBold": { + "description": "Visa rubriktexten i fetstil", + "label": "Fetstil för rubriktext" + }, + "carouselDelay": { + "description": "Tid mellan automatiskt byte av sida", + "label": "Karusellfördröjning" + }, + "compassNorthTop": { + "description": "Fixera norr till toppen av kompassen", + "label": "Kompassens norrläge" + }, + "displayMode": { + "description": "Variant på displaylayout", + "label": "Visningsläge" + }, + "displayUnits": { + "description": "Visa metriska eller Brittiska enheter", + "label": "Enheter" + }, + "flipScreen": { + "description": "Vänd displayen 180 grader", + "label": "Vänd skärmen" + }, + "gpsDisplayUnits": { + "description": "Koordinatformat för visning", + "label": "Koordinatformat" + }, + "oledType": { + "description": "Typ av OLED-skärm ansluten till enheten", + "label": "OLED-typ" + }, + "screenTimeout": { + "description": "Stäng av skärmen efter denna tid", + "label": "Tidsgräns för display" + }, + "twelveHourClock": { + "description": "Använd 12-timmarsformat", + "label": "12-timmars klocka" + }, + "wakeOnTapOrMotion": { + "description": "Väck enheten när rörelse upptäcks av enhetens accelerometer", + "label": "Vakna vid rörelse" + } + }, + "lora": { + "title": "Inställningar för nät", + "description": "Inställningar för LoRa-nätet", + "bandwidth": { + "description": "Kanalens bandbredd i MHz", + "label": "Bandbredd" + }, + "boostedRxGain": { + "description": "Ökad RX-förstärkning", + "label": "Ökad RX-förstärkning" + }, + "codingRate": { + "description": "Nämnaren för kodningshastighet", + "label": "Kodningshastighet" + }, + "frequencyOffset": { + "description": "Frekvensförskjutning för att korrigera vid kalibreringsfel i kristallen", + "label": "Frekvensförskjutning" + }, + "frequencySlot": { + "description": "LoRa-kanalnummer för frekvens", + "label": "Frekvens-slot" + }, + "hopLimit": { + "description": "Maximalt antal hopp", + "label": "Hoppgräns" + }, + "ignoreMqtt": { + "description": "Vidarebefordra inte MQTT-meddelanden över nätet", + "label": "Ignorera MQTT" + }, + "modemPreset": { + "description": "Modem-förinställningar som enheten använder", + "label": "Modem-förinställningar" + }, + "okToMqtt": { + "description": "När den är aktiverad anger denna konfiguration att användaren godkänner att paketet sänds till MQTT. Om avaktiverad uppmanas andra noder att inte vidarebefordra enhetens paket till MQTT", + "label": "OK till MQTT" + }, + "overrideDutyCycle": { + "description": "Ersätt gräns för driftsperiod", + "label": "Åsidosätt gräns för driftsperiod" + }, + "overrideFrequency": { + "description": "Åsidosätt frekvens", + "label": "Åsidosätt frekvens" + }, + "region": { + "description": "Anger regionen för din nod", + "label": "Region" + }, + "spreadingFactor": { + "description": "Indikerar antalet chirps per symbol", + "label": "Spridningsfaktor" + }, + "transmitEnabled": { + "description": "Aktivera/inaktivera sändning (TX) från LoRa-radion", + "label": "Sändning aktiverad" + }, + "transmitPower": { + "description": "Max sändningseffekt", + "label": "Sändningseffekt" + }, + "usePreset": { + "description": "Använd en av de fördefinierade modeminställningarna", + "label": "Använd förinställning" + }, + "meshSettings": { + "description": "Inställningar för LoRa-nät", + "label": "Inställningar för nät" + }, + "waveformSettings": { + "description": "Inställningar för LoRa-vågformen", + "label": "Inställningar för vågform" + }, + "radioSettings": { + "label": "Radioinställningar", + "description": "Inställningar för LoRa-radio" + } + }, + "network": { + "title": "Wifi-konfiguration", + "description": "Konfiguration av WiFi-radio", + "note": "OBS! Vissa enheter (ESP32) kan inte använda både Bluetooth och WiFi samtidigt.", + "addressMode": { + "description": "Val för tilldelning av IP-adress", + "label": "Adress-läge" + }, + "dns": { + "description": "DNS-server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Aktivera eller inaktivera Ethernet-porten", + "label": "Aktiverad" + }, + "gateway": { + "description": "Standard-gateway", + "label": "Gateway" + }, + "ip": { + "description": "IP-adress", + "label": "Ip-adress" + }, + "psk": { + "description": "Nätverkslösenord", + "label": "PSK" + }, + "ssid": { + "description": "Nätverksnamn", + "label": "SSID" + }, + "subnet": { + "description": "Subnätmask", + "label": "Subnät" + }, + "wifiEnabled": { + "description": "Aktivera eller inaktivera WiFi-radio", + "label": "Aktiverad" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP-server" + }, + "rsyslogServer": { + "label": "Rsyslog-server" + }, + "ethernetConfigSettings": { + "description": "Ethernet-portens konfiguration", + "label": "Ethernet-konfiguration" + }, + "ipConfigSettings": { + "description": "IP-konfiguration", + "label": "IP-konfiguration" + }, + "ntpConfigSettings": { + "description": "NTP-konfiguration", + "label": "NTP-konfiguration" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog-konfiguration", + "label": "Rsyslog-konfiguration" + }, + "udpConfigSettings": { + "description": "UDP över nät-konfiguration", + "label": "UDP-konfiguration" + } + }, + "position": { + "title": "Platsinställningar", + "description": "Inställningar för platsmodulen", + "broadcastInterval": { + "description": "Hur ofta din plats skickas ut över nätet", + "label": "Sändningsintervall" + }, + "enablePin": { + "description": "Stift för aktivering av GPS-modulen", + "label": "Stift för aktivering" + }, + "fixedPosition": { + "description": "Rapportera inte den inhämtade GPS-positionen, utan en manuellt angiven sådan", + "label": "Fast plats" + }, + "gpsMode": { + "description": "Konfigurera om enhetens GPS är aktiverad, inaktiverad eller saknas", + "label": "GPS-läge" + }, + "gpsUpdateInterval": { + "description": "Hur ofta en GPS-position ska inhämtas", + "label": "Intervall för GPS-uppdatering" + }, + "positionFlags": { + "description": "Valfria fält att inkludera vid sammansättning av positionsmeddelanden. Ju fler fält som väljs, desto större kommer meddelandet att bli. Längre meddelanden leder till högre sändningsutnyttnande och en högre risk för paketförlust.", + "label": "Positionsflaggor" + }, + "receivePin": { + "description": "RX-stift för GPS-modulen", + "label": "RX-stift" + }, + "smartPositionEnabled": { + "description": "Sänd bara positionsuppdatering när det har skett en tillräckligt stor förändring av positionen", + "label": "Aktivera smart position" + }, + "smartPositionMinDistance": { + "description": "Minsta avstånd (i meter) som enheten måste förflyttas innan en positionsuppdatering sänds", + "label": "Minimiavstånd för smart position" + }, + "smartPositionMinInterval": { + "description": "Minsta tidsintervall (i sekunder) som måste passera innan en positionsuppdatering skickas", + "label": "Minimiintervall för smart position" + }, + "transmitPin": { + "description": "TX-stift för GPS-modulen", + "label": "TX-stift" + }, + "intervalsSettings": { + "description": "Hur ofta enheten skickar positionsuppdateringar", + "label": "Intervall" + }, + "flags": { + "placeholder": "Välj positionsflaggor...", + "altitude": "Altitud", + "altitudeGeoidalSeparation": "Geoidhöjd", + "altitudeMsl": "Altitud är medelhavsnivå", + "dop": "Bidrag till osäkerhet i precision (DOP) PDOP används som standard", + "hdopVdop": "Om DOP är satt, använd HDOP / VDOP värden istället för PDOP", + "numSatellites": "Antal satelliter", + "sequenceNumber": "Sekvensnummer", + "timestamp": "Tidsstämpel", + "unset": "Ej inställd", + "vehicleHeading": "Rörelseriktning", + "vehicleSpeed": "Rörelsehastighet" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Används för att justera batterispänningsavläsning", + "label": "ADC faktor" + }, + "ina219Address": { + "description": "I2C-adress till INA219 batterimonitorn", + "label": "INA219-adress" + }, + "lightSleepDuration": { + "description": "Hur länge enheten kommer att vara i lätt strömsparläge", + "label": "Tid för lätt strömsparläge" + }, + "minimumWakeTime": { + "description": "Minsta tid enheten kommer att vara vaken efter att ha tagit emot ett paket", + "label": "Minsta vakna tid" + }, + "noConnectionBluetoothDisabled": { + "description": "Om enheten inte har en aktiv Bluetooth-anslutning inaktiveras Bluetooth-radion efter så här lång tid", + "label": "Bluetooth inaktiverad vid frånkoppling" + }, + "powerSavingEnabled": { + "description": "Välj om du strömmatar enheten från en lågströmkälla (sol) för att minimera strömförbrukningen så mycket som möjligt.", + "label": "Aktivera strömsparläge" + }, + "shutdownOnBatteryDelay": { + "description": "Automatisk avstängning av enheten efter att ha strömmatats från batteri så här lång tid, 0 för obestämd tid", + "label": "Fördröjd avstängning vid batteridrift" + }, + "superDeepSleepDuration": { + "description": "Hur länge enheten kommer att vara i djupt strömsparläge", + "label": "Tid för djupt strömsparläge" + }, + "powerConfigSettings": { + "description": "Inställningar för strömmodulen", + "label": "Ströminställningar" + }, + "sleepSettings": { + "description": "Strömsparlägesinställningar för strömmodulen", + "label": "Inställningar för strömsparläge" + } + }, + "security": { + "description": "Inställningar för säkerhet", + "title": "Säkerhetsinställningar", + "button_backupKey": "Säkerhetskopiera nyckel", + "adminChannelEnabled": { + "description": "Tillåt inkommande styrning via den förlegade, osäkra, administrationskanalen", + "label": "Tillåt förlegad administration" + }, + "enableDebugLogApi": { + "description": "Skriv felsökningsloggar över seriell kommunikation samt visa och exportera positions-rensade loggar över Bluetooth", + "label": "Aktivera API för felsökningslogg" + }, + "managed": { + "description": "Om aktiverad, kan enhetens konfigurationsalternativ bara ändras på distans av en fjärradministratörsnod via administratörsmeddelanden. Aktivera inte detta alternativ om inte minst en lämplig fjärradministratörsnod har konfigurerats och den publika nyckeln matats in i något av fälten ovan.", + "label": "Fjärrhanterad" + }, + "privateKey": { + "description": "Används för att skapa en delad nyckel med en fjärrnod", + "label": "Privat nyckel" + }, + "publicKey": { + "description": "Sänds ut till andra noder på nätet för att de ska kunna beräkna en delad hemlig nyckel", + "label": "Publik nyckel" + }, + "primaryAdminKey": { + "description": "Den primära publika nyckeln hos en fjärradministratörsnod auktoriserad att skicka administratörsmeddelanden till den här noden", + "label": "Primär administratörsnyckel" + }, + "secondaryAdminKey": { + "description": "Den sekundära publika nyckeln hos en fjärradministratörsnod auktoriserad att skicka administratörsmeddelanden till den här noden", + "label": "Sekundär administratörsnyckel" + }, + "serialOutputEnabled": { + "description": "Seriell kommunikation över Stream API", + "label": "Seriell kommunikation aktiverad" + }, + "tertiaryAdminKey": { + "description": "Den tertiära publika nyckeln hos en fjärradministratörsnod auktoriserad att skicka administratörsmeddelanden till den här noden", + "label": "Tertiär administratörsnyckel" + }, + "adminSettings": { + "description": "Inställningar för fjärradministration", + "label": "Fjärradministrationsinställningar" + }, + "loggingSettings": { + "description": "Inställningar för loggning", + "label": "Loggningsinställningar" + } + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/dialog.json b/packages/web/public/i18n/locales/sv-SE/dialog.json new file mode 100644 index 00000000..0ed123b8 --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "Den här åtgärden kommer att rensa all meddelandehistorik. Detta kan inte ångras. Är du säker på att du vill fortsätta?", + "title": "Rensa alla meddelanden" + }, + "deviceName": { + "description": "Enheten kommer att starta om när inställningarna har sparats.", + "longName": "Långt namn", + "shortName": "Kort namn", + "title": "Ändra enhetens namn" + }, + "import": { + "description": "Den aktuella LoRa konfigurationen kommer att skrivas över.", + "error": { + "invalidUrl": "Ogiltig Meshtastic URL" + }, + "channelPrefix": "Kanal: ", + "channelSetUrl": "Kanalinställning/QR-kod URL", + "channels": "Kanaler:", + "usePreset": "Använd förinställning?", + "title": "Importera kanaluppsättning" + }, + "locationResponse": { + "altitude": "Höjd:", + "coordinates": "Koordinater: ", + "title": "Plats: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Förnya nyckel?", + "description": "Är du säker på att du vill förnya den fördelade nyckeln?", + "regenerate": "Förnya" + }, + "newDeviceDialog": { + "title": "Anslut ny enhet", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Seriell kommunikation", + "useHttps": "Använd HTTPS", + "connecting": "Ansluter...", + "connect": "Anslut", + "connectionFailedAlert": { + "title": "Anslutningen misslyckades", + "descriptionPrefix": "Kunde inte ansluta till enheten. ", + "httpsHint": "Om du använder HTTPS kan du behöva godkänna ett självsignerat certifikat först. ", + "openLinkPrefix": "Vänligen öppna ", + "openLinkSuffix": " i en ny flik", + "acceptTlsWarningSuffix": ", acceptera eventuella TLS-varningar om du uppmanas och försök igen", + "learnMoreLink": "Läs mer" + }, + "httpConnection": { + "label": "IP-adress/värdnamn", + "placeholder": "000.000.000.000 / meshtastic.lokal" + }, + "serialConnection": { + "noDevicesPaired": "Inga enheter parkopplade ännu.", + "newDeviceButton": "Ny enhet", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "Inga enheter parkopplade ännu.", + "newDeviceButton": "Ny enhet" + }, + "validation": { + "requiresWebBluetooth": "Den här anslutningstypen kräver <0>Web Bluetooth. Använd en webbläsare som stöds, till exempel Chrome eller Edge.", + "requiresWebSerial": "Den här anslutningstypen kräver <0>Web Serial. Använd en webbläsare som stöds, till exempel Chrome eller Edge.", + "requiresSecureContext": "Denna applikation kräver en <0>säker kontext. Anslut med HTTPS eller localhost.", + "additionallyRequiresSecureContext": "Dessutom kräver den ett <0>säker kontext. Vänligen anslut med HTTPS eller localhost." + } + }, + "nodeDetails": { + "message": "Meddelande", + "requestPosition": "Begär plats", + "traceRoute": "Spåra rutt", + "airTxUtilization": "Sändningsutnyttjande", + "allRawMetrics": "All oformaterad data", + "batteryLevel": "Batterinivå", + "channelUtilization": "Kanalutnyttjande", + "details": "Detaljer:", + "deviceMetrics": "Enhetens mätvärden:", + "hardware": "Hårdvara: ", + "lastHeard": "Senast hörd: ", + "nodeHexPrefix": "Nod-hex: !", + "nodeNumber": "Nodnummer: ", + "position": "Plats:", + "role": "Roll: ", + "uptime": "Drifttid: ", + "voltage": "Spänning", + "title": "Nodinformation för {{identifier}}", + "ignoreNode": "Ignorera nod", + "removeNode": "Ta bort nod", + "unignoreNode": "Ta bort favoritmarkering" + }, + "pkiBackup": { + "loseKeysWarning": "Om du förlorar dina nycklar måste du återställa din enhet.", + "secureBackup": "Det är viktigt att säkerhetskopiera dina publika och privata nycklar och förvara din säkerhetskopia säkert.", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Privat nyckel:", + "publicKey": "Publik nyckel:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Säkerhetskopiera nycklar" + }, + "pkiBackupReminder": { + "description": "Vi rekommenderar att du säkerhetskopierar dina nycklar regelbundet. Vill du säkerhetskopiera nu?", + "title": "Påminnelse om säkerhetskopiering", + "remindLaterPrefix": "Påminn mig om", + "remindNever": "Påminn mig aldrig", + "backupNow": "Säkerhetskopiera nu" + }, + "pkiRegenerate": { + "description": "Är du säker på att du vill förnya nyckelpar?", + "title": "Förnya nyckelpar" + }, + "qr": { + "addChannels": "Lägg till kanaler", + "replaceChannels": "Ersätt kanaler", + "description": "Den aktuella LoRa-konfigurationen kommer också att delas.", + "sharableUrl": "Delbar URL", + "title": "Generera QR-kod" + }, + "rebootOta": { + "title": "Schemalägg omstart", + "description": "Starta om den anslutna noden till OTA- (Over-the-Air) läge efter en fördröjning.", + "enterDelay": "Ange fördröjning (sekunder)", + "scheduled": "Omstart har schemalagts" + }, + "reboot": { + "title": "Schemalägg omstart", + "description": "Starta om den anslutna noden efter x minuter." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "Detta kommer att ta bort noden från enheten och begära nya nycklar.", + "keyMismatchReasonSuffix": ". Detta beror på att fjärrnodens nuvarande publika nyckel inte matchar den tidigare lagrade nyckeln för den här noden.", + "unableToSendDmPrefix": "Din nod kan inte skicka ett direkt meddelande till noden: " + }, + "acceptNewKeys": "Godkänn nya nycklar", + "title": "Nycklarna matchar inte - {{identifier}}" + }, + "removeNode": { + "description": "Är du säker på att du vill ta bort den här noden?", + "title": "Ta bort noden?" + }, + "shutdown": { + "title": "Schemalägg avstängning", + "description": "Stäng av den anslutna noden efter x minuter." + }, + "traceRoute": { + "routeToDestination": "Rutt till destination:", + "routeBack": "Rutt tillbaka:" + }, + "tracerouteResponse": { + "title": "Spåra rutt: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Ja, jag vet vad jag gör", + "conjunction": " och blogginlägget om ", + "postamble": " och förstår konsekvenserna av att ändra roll.", + "preamble": "Jag har läst ", + "choosingRightDeviceRole": "Att välja rätt enhetsroll", + "deviceRoleDocumentation": "Dokumentation för enhetsroll", + "title": "Är du säker?" + }, + "managedMode": { + "confirmUnderstanding": "Ja, jag vet vad jag gör", + "title": "Är du säker?", + "description": "Aktivering av fjärrhanterat läge blockerar klienter (inklusive webbklienten) från att skriva inställningar till en radio. När detta är aktiverat kan enhetens inställningar endast ändras via fjärradministratörsmeddelanden. Den här inställningen krävs inte för fjärradministration." + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/messages.json b/packages/web/public/i18n/locales/sv-SE/messages.json new file mode 100644 index 00000000..203b600f --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Meddelanden: {{chatName}}", + "placeholder": "Ange meddelande" + }, + "emptyState": { + "title": "Välj en chatt", + "text": "Det finns inga meddelanden ännu." + }, + "selectChatPrompt": { + "text": "Välj en kanal eller nod för att börja chatta." + }, + "sendMessage": { + "placeholder": "Ange ditt meddelande här...", + "sendButton": "Skicka" + }, + "actionsMenu": { + "addReactionLabel": "Lägg till reaktion", + "replyLabel": "Svara" + }, + "deliveryStatus": { + "delivered": { + "label": "Meddelandet har levererats", + "displayText": "Meddelandet har levererats" + }, + "failed": { + "label": "Meddelandeleverans misslyckades", + "displayText": "Leverans misslyckades" + }, + "unknown": { + "label": "Okänd meddelandestatus", + "displayText": "Okänd status" + }, + "waiting": { + "label": "Skickar meddelande", + "displayText": "Väntar på leverans" + } + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/moduleConfig.json b/packages/web/public/i18n/locales/sv-SE/moduleConfig.json new file mode 100644 index 00000000..075451c9 --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Omgivande belysning", + "tabAudio": "Ljud", + "tabCannedMessage": "Fördefinierade meddelanden", + "tabDetectionSensor": "Detekteringssensor", + "tabExternalNotification": "Extern avisering", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Granninformation", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Räckvidd", + "tabSerial": "Seriell kommunikation", + "tabStoreAndForward": "Lagra & vidarebefodra", + "tabTelemetry": "Telemetri" + }, + "ambientLighting": { + "title": "Inställningar för omgivande belysning", + "description": "Inställningar för modulen för omgivande belysning", + "ledState": { + "label": "LED-läge", + "description": "Anger om lysdioden ska vara på eller av" + }, + "current": { + "label": "Ström", + "description": "Ställer in strömmen för LED-utgången. Standardvärdet är 10" + }, + "red": { + "label": "Rött", + "description": "Anger nivån för den röda lysdioden. Värdena är 0-255" + }, + "green": { + "label": "Grönt", + "description": "Anger nivån för den gröna lysdioden. Värdena är 0-255" + }, + "blue": { + "label": "Blått", + "description": "Anger nivån för den blåa lysdioden. Värdena är 0-255" + } + }, + "audio": { + "title": "Ljudinställningar", + "description": "Inställningar för ljudmodulen", + "codec2Enabled": { + "label": "Codec 2 aktiverad", + "description": "Aktivera Codec 2-ljudkodning" + }, + "pttPin": { + "label": "PTT-stift", + "description": "GPIO-stift för PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate att använda för ljudkodning" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO-stift att använda för i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO-stift att använda för i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO-stift att använda för i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO-stift att använda för i2S SCK" + } + }, + "cannedMessage": { + "title": "Inställningar för fördefinierade meddelanden", + "description": "Inställningar för modulen för fördefinierade meddelanden", + "moduleEnabled": { + "label": "Modul aktiverad", + "description": "Aktivera fördefinierade meddelanden" + }, + "rotary1Enabled": { + "label": "Vridomkopplare #1 aktiverad", + "description": "Aktivera vridomkopplare" + }, + "inputbrokerPinA": { + "label": "Vridomkopplarstift A", + "description": "GPIO-stift (1-39) för vridomkopplarens A-stift" + }, + "inputbrokerPinB": { + "label": "Vridomkopplarstift B", + "description": "GPIO-stift (1-39) för vridomkopplarens B-stift" + }, + "inputbrokerPinPress": { + "label": "Vridomkopplarens knappstift", + "description": "GPIO-stift (1-39) för vridomkopplarens knapp-stift" + }, + "inputbrokerEventCw": { + "label": "Medurs inmatningshändelse", + "description": "Välj inmatningshändelse." + }, + "inputbrokerEventCcw": { + "label": "Moturs inmatningshändelse", + "description": "Välj inmatningshändelse." + }, + "inputbrokerEventPress": { + "label": "Inmatningshändelse för tryck", + "description": "Välj inmatningshändelse." + }, + "updown1Enabled": { + "label": "Upp/ned aktiverad", + "description": "Aktivera upp/ner vridomkopplaren" + }, + "allowInputSource": { + "label": "Tillåten inmatningskälla", + "description": "Välj från: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Skicka klocka", + "description": "Skickar det speciella klock-tecknet med varje meddelande" + } + }, + "detectionSensor": { + "title": "Inställningar för detekteringssensor", + "description": "Inställningar för detekteringssensor-modulen", + "enabled": { + "label": "Aktiverad", + "description": "Aktivera eller inaktivera detekteringssensor-modulen" + }, + "minimumBroadcastSecs": { + "label": "Minsta sändningsintervall", + "description": "Tidsintervall i sekunder hur ofta vi kan skicka ett meddelande till nätet när en tillståndsändring upptäckts" + }, + "stateBroadcastSecs": { + "label": "Högsta sändningsintervall", + "description": "Tidsintervall i sekunder hur ofta vi ska skicka ett meddelande till nätet med nuvarande tillstånd oavsett eventuella förändringar" + }, + "sendBell": { + "label": "Skicka klocka", + "description": "Skicka det speciella klock-tecknet med meddelandet" + }, + "name": { + "label": "Visningsnamn", + "description": "Används för att formatera meddelandet som skickas till nätet, max 20 tecken" + }, + "monitorPin": { + "label": "Stift att övervaka", + "description": "GPIO-stiftet att övervaka för tillståndsändringar" + }, + "detectionTriggerType": { + "label": "Typ för utlösande händelse", + "description": "Typ av utlösande händelse som ska användas" + }, + "usePullup": { + "label": "Använd Pullup", + "description": "Huruvida INPUT_PULLUP-läget ska användas för GPIO-stiftet " + } + }, + "externalNotification": { + "title": "Inställningar för extern avisering", + "description": "Konfigurera modulen för extern avisering", + "enabled": { + "label": "Modul aktiverad", + "description": "Aktivera extern avisering" + }, + "outputMs": { + "label": "Längd", + "description": "Hur länge ska notifikationen vara aktiverad" + }, + "output": { + "label": "Utgång", + "description": "GPIO-stift för utgång" + }, + "outputVibra": { + "label": "Vibrationsutgång", + "description": "GPIO-stift för vibrationsutgång" + }, + "outputBuzzer": { + "label": "Summerutgång", + "description": "GPIO-stift för summerutgång" + }, + "active": { + "label": "Aktiv", + "description": "Aktiv" + }, + "alertMessage": { + "label": "Meddelande-utgång", + "description": "Aktivera utgång när nytt meddelande tas emot" + }, + "alertMessageVibra": { + "label": "Meddelande-vibrationsutgång", + "description": "Aktivera vibrationsutgång när nytt meddelande tas emot" + }, + "alertMessageBuzzer": { + "label": "Meddelande-summer", + "description": "Aktivera summerutgång när nytt meddelande tas emot" + }, + "alertBell": { + "label": "Klock-tecken-utgång", + "description": "Aktivera utgång när ett inkommande meddelande innehåller det speciella klock-tecknet?" + }, + "alertBellVibra": { + "label": "Klock-tecken-vibration", + "description": "Aktivera vibrationsutgång när ett inkommande meddelande innehåller det speciella klock-tecknet" + }, + "alertBellBuzzer": { + "label": "Klock-tecken-summer", + "description": "Aktivera summerutgång när ett inkommande meddelande innehåller det speciella klock-tecknet" + }, + "usePwm": { + "label": "Använd PWM", + "description": "Använd pulsbreddsmodulering (PWM)" + }, + "nagTimeout": { + "label": "Time-out för påminnelse", + "description": "Time-out för påminnelse" + }, + "useI2sAsBuzzer": { + "label": "Använd I²S-stift som summer", + "description": "Ange I²S-stift som summerutgång" + } + }, + "mqtt": { + "title": "MQTT Inställningar", + "description": "Inställningar för MQTT-modulen", + "enabled": { + "label": "Aktiverad", + "description": "Aktivera eller inaktivera MQTT" + }, + "address": { + "label": "MQTT-serveradress", + "description": "MQTT-serveradress att använda för standard/anpassade servrar" + }, + "username": { + "label": "MQTT-användarnamn", + "description": "MQTT-användarnamn att använda för standard/anpassade servrar" + }, + "password": { + "label": "MQTT-lösenord", + "description": "MQTT-lösenord att använda för standard/anpassade servrar" + }, + "encryptionEnabled": { + "label": "Aktivera kryptering ", + "description": "Aktivera eller inaktivera MQTT-kryptering. OBS: Alla meddelanden skickas okrypterade till MQTT-servern om det här alternativet inte är aktiverat, även när dina kanaler med aktiverad MQTT-upplänk har krypteringsnycklar. Detta inkluderar positionsdata." + }, + "jsonEnabled": { + "label": "JSON aktiverat", + "description": "Om du vill skicka/ta emot JSON-paket på MQTT" + }, + "tlsEnabled": { + "label": "TLS aktiverat", + "description": "Aktivera eller inaktivera TLS" + }, + "root": { + "label": "Rotämne (root topic)", + "description": "MQTT rotämne (root topic) att använda för standard/anpassade servrar " + }, + "proxyToClientEnabled": { + "label": "MQTT-klientproxy aktiverad", + "description": "Utnyttja nätverksanslutningen för att vidarebefodra MQTT-meddelanden till klienten." + }, + "mapReportingEnabled": { + "label": "Kartrapportering aktiverad", + "description": "Din nod kommer periodvis skicka ett okrypterat paket till den konfigurerade MQTT-servern som inkluderar id, kort och långt namn, ungefärlig plats, hardvarumodell, enhetsroll, mjukvaru-version, LoRa-region, modeminställning och den primära kanalens namn." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Intervall för kartrapportering", + "description": "Intervall i sekunder för hur ofta kartrapportering ska ske" + }, + "positionPrecision": { + "label": "Ungefärlig plats", + "description": "Den delade platsen kommer att avrundas till denna noggrannhet", + "options": { + "metric_km23": "Inom 23 km", + "metric_km12": "Inom 12 km", + "metric_km5_8": "Inom 5,8 km", + "metric_km2_9": "Inom 2,9 km", + "metric_km1_5": "Inom 1,5 km", + "metric_m700": "Inom 700 m", + "metric_m350": "Inom 350 m", + "metric_m200": "Inom 200 m", + "metric_m90": "Inom 90 meter", + "metric_m50": "inom 50 m", + "imperial_mi15": "Inom 15 engelska mil", + "imperial_mi7_3": "Inom 7,3 engelska mil", + "imperial_mi3_6": "Inom 3,6 engelska mil", + "imperial_mi1_8": "Inom 1,8 engelska mil", + "imperial_mi0_9": "Inom 0,9 engelska mil", + "imperial_mi0_5": "Inom 0,5 engelska mil", + "imperial_mi0_2": "Inom 0,2 engelska mil", + "imperial_ft600": "Inom 600 fot", + "imperial_ft300": "Inom 300 fot", + "imperial_ft150": "Inom 150 fot" + } + } + } + }, + "neighborInfo": { + "title": "Inställningar för granninformation", + "description": "Inställningar för granninformation-modulen", + "enabled": { + "label": "Aktiverad", + "description": "Aktivera eller inaktivera granninformation-modulen" + }, + "updateInterval": { + "label": "Uppdateringsintervall", + "description": "Intervall i sekunder av hur ofta granninformation ska försöka skickas till nätet" + } + }, + "paxcounter": { + "title": "Inställningar för Paxcounter", + "description": "Inställningar för Paxcounter-modulen", + "enabled": { + "label": "Modul aktiverad", + "description": "Aktivera Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Uppdateringsintervall (sekunder)", + "description": "Hur lång tid mellan paxcounter paket skickas" + }, + "wifiThreshold": { + "label": "Tröskelvärde för WiFi RSSI ", + "description": "På vilken WiFi RSSI-nivå ska räknaren öka. Standardvärdet är -80." + }, + "bleThreshold": { + "label": "Tröskelvärde för Bluetooth RSSI ", + "description": "På vilken Bluetooth RSSI-nivå ska räknaren öka. Standardvärdet är -80." + } + }, + "rangeTest": { + "title": "Inställningar för räckviddstest", + "description": "Inställningar för räckviddstest-modulen", + "enabled": { + "label": "Modul aktiverad", + "description": "Aktivera räckviddstest" + }, + "sender": { + "label": "Meddelandeintervall", + "description": "Hur lång tid mellan utsändning av testpaket" + }, + "save": { + "label": "Spara CSV till lagring", + "description": "Spara CSV till enhetens egna flashminne. Endast tillgängligt på ESP32-enheter." + } + }, + "serial": { + "title": "Inställningar för seriell kommunikation", + "description": "Inställningar för den seriella modulen", + "enabled": { + "label": "Modul aktiverad", + "description": "Aktivera seriell utmatning" + }, + "echo": { + "label": "Eko", + "description": "Alla paket du skickar kommer att upprepas tillbaka till din enhet" + }, + "rxd": { + "label": "Stift för mottagning", + "description": "Ställ in GPIO-stift till det RXD-stift du har konfigurerat." + }, + "txd": { + "label": "Stift för sändning", + "description": "Ställ in GPIO-stift till det TXD-stift du har konfigurerat." + }, + "baud": { + "label": "Hastighet", + "description": "Den seriella kommunikationens hastighet" + }, + "timeout": { + "label": "Timeout", + "description": "Tid att vänta innan ditt paket betraktas som \"klart\"." + }, + "mode": { + "label": "Typ", + "description": "Välj typ av paket" + }, + "overrideConsoleSerialPort": { + "label": "Åsidosätt konsolens serieport", + "description": "Om du har en seriell port ansluten till konsolen kommer detta att ersätta den." + } + }, + "storeForward": { + "title": "Inställningar för lagra & vidarebefodra", + "description": "Inställningar för lagra & vidarebefodra-modulen", + "enabled": { + "label": "Modul aktiverad", + "description": "Aktivera lagra & vidarebefodra-modulen" + }, + "heartbeat": { + "label": "Puls aktiverad", + "description": "Aktivera sändning av lagra & vidarebefodra-puls " + }, + "records": { + "label": "Antal poster", + "description": "Antal poster att lagra" + }, + "historyReturnMax": { + "label": "Maxstorlek för historik", + "description": "Maximalt antal poster att returnera" + }, + "historyReturnWindow": { + "label": "Returfönstrets storlek för historik", + "description": "Maximalt antal poster att returnera" + } + }, + "telemetry": { + "title": "Inställningar för telemetri", + "description": "Inställningar för telemetrimodulen", + "deviceUpdateInterval": { + "label": "Enhetens mätvärden", + "description": "Uppdateringsintervall för enhetsdata" + }, + "environmentUpdateInterval": { + "label": "Uppdateringsintervall för miljömätningar", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Modul aktiverad", + "description": "Aktivera miljötelemetri" + }, + "environmentScreenEnabled": { + "label": "Visa på display", + "description": "Visa telemetri-värden på OLED-displayen" + }, + "environmentDisplayFahrenheit": { + "label": "Visa Fahrenheit", + "description": "Visa temperatur i Fahrenheit" + }, + "airQualityEnabled": { + "label": "Aktivera luftkvalitet ", + "description": "Aktivera telemetri för luftkvalitet" + }, + "airQualityInterval": { + "label": "Intervall för uppdatering av luftkvalitet", + "description": "Hur ofta att skicka data om luftkvalitet över nätet" + }, + "powerMeasurementEnabled": { + "label": "Effektmätning aktiverad", + "description": "Aktivera telemetri för effektmätning" + }, + "powerUpdateInterval": { + "label": "Intervall för effektuppdatering", + "description": "Hur ofta ska effektdata skickas över nätet" + }, + "powerScreenEnabled": { + "label": "Aktivera strömskärm", + "description": "Aktivera skärmen för strömdata" + } + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/nodes.json b/packages/web/public/i18n/locales/sv-SE/nodes.json new file mode 100644 index 00000000..97855c4c --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Publik nyckel aktiverad" + }, + "noPublicKey": { + "label": "Publik nyckel saknas" + }, + "directMessage": { + "label": "Direktmeddelande {{shortName}}" + }, + "favorite": { + "label": "Favorit", + "tooltip": "Lägg till eller ta bort den här noden från dina favoriter" + }, + "notFavorite": { + "label": "Inte en favorit" + }, + "error": { + "label": "Fel", + "text": "Ett fel inträffade vid hämtning av nodens detaljer. Försök igen senare." + }, + "status": { + "heard": "Hörd", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Höjd" + }, + "channelUtil": { + "label": "Kanalutnyttjande" + }, + "airtimeUtil": { + "label": "Sändningstidsutnyttjande" + } + }, + "nodesTable": { + "headings": { + "longName": "Långt namn", + "connection": "Anslutning", + "lastHeard": "Senast hörd", + "encryption": "Kryptering", + "model": "Modell", + "macAddress": "MAC-adress" + }, + "connectionStatus": { + "direct": "Direkt", + "away": "bort", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Aldrig" + } + }, + "actions": { + "added": "Tillagd", + "removed": "Borttagen", + "ignoreNode": "Ignorera nod", + "unignoreNode": "Ta bort ignorering", + "requestPosition": "Begär plats" + } +} diff --git a/packages/web/public/i18n/locales/sv-SE/ui.json b/packages/web/public/i18n/locales/sv-SE/ui.json new file mode 100644 index 00000000..dee4cf1e --- /dev/null +++ b/packages/web/public/i18n/locales/sv-SE/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigering", + "messages": "Meddelanden", + "map": "Karta", + "config": "Inställningar", + "radioConfig": "Radioinställningar", + "moduleConfig": "Modulinställningar", + "channels": "Kanaler", + "nodes": "Noder" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic logotyp" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Öppna sidopanel", + "close": "Stäng sidopanel" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volt", + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Kompileringsdatum: {{date}}" + }, + "deviceName": { + "title": "Enhetsnamn", + "changeName": "Ändra enhetens namn", + "placeholder": "Ange enhetsnamn" + }, + "editDeviceName": "Redigera enhetsnamn" + } + }, + "batteryStatus": { + "charging": "{{level}}%, laddar", + "pluggedIn": "Ansluten", + "title": "Batteri" + }, + "search": { + "nodes": "Sök noder...", + "channels": "Sök kanaler...", + "commandPalette": "Sök kommandon..." + }, + "toast": { + "positionRequestSent": { + "title": "Platsbegäran skickad." + }, + "requestingPosition": { + "title": "Begär plats, var god vänta..." + }, + "sendingTraceroute": { + "title": "Skickar Traceroute (spåra rutt), vänligen vänta..." + }, + "tracerouteSent": { + "title": "Traceroute (spåra rutt) skickat.." + }, + "savedChannel": { + "title": "Sparat kanal: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chatten använder PKI-kryptering." + }, + "pskEncryption": { + "title": "Chatten använder PSK-kryptering." + } + }, + "configSaveError": { + "title": "Kunde inte spara inställningarna", + "description": "Ett fel inträffade när inställningarna sparades." + }, + "validationError": { + "title": "Det förekommer inställningsfel", + "description": "Vänligen åtgärda felen i inställningarna innan du sparar." + }, + "saveSuccess": { + "title": "Sparar inställningarna", + "description": "Ändrignarna av {{case}}-inställningarna har sparats." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} dina favoriter.", + "action": { + "added": "La till", + "removed": "Tog bort", + "to": "till", + "from": "från" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} dina ignorerade noder", + "action": { + "added": "La till", + "removed": "Tog bort", + "to": "till", + "from": "från" + } + } + }, + "notifications": { + "copied": { + "label": "Kopierat!" + }, + "copyToClipboard": { + "label": "Kopiera till Urklipp" + }, + "hidePassword": { + "label": "Dölj lösenord" + }, + "showPassword": { + "label": "Visa lösenord" + }, + "deliveryStatus": { + "delivered": "Levererad", + "failed": "Leverans misslyckades", + "waiting": "Väntar", + "unknown": "Okänd" + } + }, + "general": { + "label": "Allmänt" + }, + "hardware": { + "label": "Hårdvara" + }, + "metrics": { + "label": "Telemetri" + }, + "role": { + "label": "Roll" + }, + "filter": { + "label": "Filter" + }, + "advanced": { + "label": "Advancerat" + }, + "clearInput": { + "label": "Rensa inmatning" + }, + "resetFilters": { + "label": "Återställ filter" + }, + "nodeName": { + "label": "Nodnamn/nummer", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Sändningstidsutnyttjande (%)" + }, + "batteryLevel": { + "label": "Batterinivå (%)", + "labelText": "Batterinivå (%): {{value}}" + }, + "batteryVoltage": { + "label": "Batterispänning (V)", + "title": "Spänning" + }, + "channelUtilization": { + "label": "Kanalutnyttjande (%)" + }, + "hops": { + "direct": "Direkt", + "label": "Antal hopp", + "text": "Antal hopp: {{value}}" + }, + "lastHeard": { + "label": "Senast hörd", + "labelText": "Senast hörd: {{value}}", + "nowLabel": "Nu" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favoriter" + }, + "hide": { + "label": "Dölj" + }, + "showOnly": { + "label": "Visa endast" + }, + "viaMqtt": { + "label": "Ansluten via MQTT" + }, + "hopsUnknown": { + "label": "Okänt antal hopp" + }, + "showUnheard": { + "label": "Aldrig hörd" + }, + "language": { + "label": "Språk", + "changeLanguage": "Byt språk" + }, + "theme": { + "dark": "Mörkt", + "light": "Ljust", + "system": "Automatisk", + "changeTheme": "Ändra färgschema" + }, + "errorPage": { + "title": "Det här är lite pinsamt...", + "description1": "Vi är verkligen ledsna men ett fel inträffade i webbklienten som fick den att krascha.
Detta var inte meningen, men vi arbetar hårt för att åtgärda det.", + "description2": "Det bästa sättet att förhindra att detta händer igen för dig eller någon annan är att rapportera problemet till oss.", + "reportInstructions": "Vänligen inkludera följande information i din rapport:", + "reportSteps": { + "step1": "Vad du gjorde när felet inträffade", + "step2": "Vad du förväntade dig skulle hända", + "step3": "Vad som faktiskt hände", + "step4": "All annan relevant information" + }, + "reportLink": "Du kan rapportera problemet på vår <0>GitHub", + "dashboardLink": "Återvänd till <0>start", + "detailsSummary": "Detaljer om felet", + "errorMessageLabel": "Felmeddelande:", + "stackTraceLabel": "Stackspårning:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Drivs av <0>▲ Vercel | Meshtastic® är ett registrerat varumärke som tillhör Meshtastic LLC. | <1>Juridisk information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/channels.json b/packages/web/public/i18n/locales/tr-TR/channels.json new file mode 100644 index 00000000..74fe0384 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Kanallar", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "Birincil", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Kanal Ayarları", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Rol", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "İsmi", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Location", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/commandPalette.json b/packages/web/public/i18n/locales/tr-TR/commandPalette.json new file mode 100644 index 00000000..cc73c62a --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Mesajlar", + "map": "Harita", + "config": "Config", + "channels": "Kanallar", + "nodes": "Nodes" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "İçeri aktar", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Hata Ayıklama", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/common.json b/packages/web/public/i18n/locales/tr-TR/common.json new file mode 100644 index 00000000..3fab7218 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Uygula", + "backupKey": "Backup Key", + "cancel": "İptal", + "clearMessages": "Clear Messages", + "close": "Kapat", + "confirm": "Confirm", + "delete": "Sil", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "İçeri aktar", + "message": "Mesaj", + "now": "Now", + "ok": "Tamam", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Kaldır", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Sıfırla", + "save": "Kaydet", + "scanQr": "QR Kodu Tara", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "Hour", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/dashboard.json b/packages/web/public/i18n/locales/tr-TR/dashboard.json new file mode 100644 index 00000000..003f8732 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Seri", + "connectionType_network": "Ağ", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/deviceConfig.json b/packages/web/public/i18n/locales/tr-TR/deviceConfig.json new file mode 100644 index 00000000..2f21752b --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "Bluetooth", + "tabDevice": "Cihaz", + "tabDisplay": "Ekran", + "tabLora": "LoRa", + "tabNetwork": "Ağ", + "tabPosition": "Konum", + "tabPower": "Güç", + "tabSecurity": "Güvenlik" + }, + "sidebar": { + "label": "Modüller" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX Zaman Dilimi" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Rol" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "Enabled" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Eşleştirme Modu" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Bant genişliği" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "MQTT'yi Yoksay" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Modem Preset" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "MQTT'ye Tamam" + }, + "overrideDutyCycle": { + "description": "Görev Döngüsünü Geçersiz Kıl", + "label": "Görev Döngüsünü Geçersiz Kıl" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Bölge" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Enabled" + }, + "gateway": { + "description": "Default Gateway", + "label": "Ağ geçidi" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "Alt ağ" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "Enabled" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "UDP Ayarları" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Zaman Damgası", + "unset": "Ayarlanmamış", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Güç tasarrufu modunu etkinleştir" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Güç Ayarı" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Özel Anahtar" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Genel Anahtar" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/dialog.json b/packages/web/public/i18n/locales/tr-TR/dialog.json new file mode 100644 index 00000000..1ba90500 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Long Name", + "shortName": "Short Name", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Seri", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "Bağlan", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Mesaj", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Voltaj", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Generate QR Code" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Emin misiniz?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "Emin misiniz?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/messages.json b/packages/web/public/i18n/locales/tr-TR/messages.json new file mode 100644 index 00000000..7bdb2d22 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "Gönder" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "Reply" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/moduleConfig.json b/packages/web/public/i18n/locales/tr-TR/moduleConfig.json new file mode 100644 index 00000000..3ba00a84 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Ortam Işıklandırması", + "tabAudio": "Ses", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Algılama Sensörü", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Komşu Bilgisi", + "tabPaxcounter": "Pax sayacı", + "tabRangeTest": "Mesafe Testi", + "tabSerial": "Seri", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Telemetri" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Akım", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Kırmızı", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Yeşil", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Mavi", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Ana konu", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Zaman Aşımı", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Kayıt sayısı", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "Geçmiş geri dönüş maksimum", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "Geçmiş geri dönüş penceresi", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Cihaz metrikleri güncelleme aralığı (saniye)" + }, + "environmentUpdateInterval": { + "label": "Çevre metrikleri güncelleme aralığı (saniye)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/nodes.json b/packages/web/public/i18n/locales/tr-TR/nodes.json new file mode 100644 index 00000000..04a314a1 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Favori", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Hata", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Long Name", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "Model", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "Doğrudan", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "Ignore Node", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/tr-TR/ui.json b/packages/web/public/i18n/locales/tr-TR/ui.json new file mode 100644 index 00000000..efbdbfb6 --- /dev/null +++ b/packages/web/public/i18n/locales/tr-TR/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "Mesajlar", + "map": "Harita", + "config": "Config", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "Kanallar", + "nodes": "Nodes" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "Yazılım", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Pil" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Şifreyi gizle" + }, + "showPassword": { + "label": "Şifreyi göster" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "Waiting", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Donanım" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Rol" + }, + "filter": { + "label": "Filtre" + }, + "advanced": { + "label": "Advanced" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Voltaj" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Doğrudan", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Son duyulma", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Dil", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "Koyu", + "light": "Açık", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/channels.json b/packages/web/public/i18n/locales/uk-UA/channels.json new file mode 100644 index 00000000..8eabfa03 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "Канали", + "channelName": "Канал: {{channelName}}", + "broadcastLabel": "Основний", + "channelIndex": "Кн {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "Channel Settings", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "Роль", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Згенерувати" + }, + "name": { + "label": "Ім'я", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "Uplink Enabled", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "Downlink Enabled", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "Місце розташування", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Не повідомляти місце знаходження", + "precise": "Точне місце розташування", + "metric_km23": "У межах 23 км", + "metric_km12": "У межах 12 км", + "metric_km5_8": "У межах 5,8 км", + "metric_km2_9": "У межах 2,9 км", + "metric_km1_5": "У межах 1,5 км", + "metric_m700": "У межах 700 м", + "metric_m350": "У межах 350 м", + "metric_m200": "У межах 200 м", + "metric_m90": "У межах 90 м", + "metric_m50": "У межах 50 м", + "imperial_mi15": "У межах 15 миль", + "imperial_mi7_3": "У межах 7,3 милі", + "imperial_mi3_6": "У межах 3,6 милі", + "imperial_mi1_8": "У межах 1,8 милі", + "imperial_mi0_9": "У межах 0,9 милі", + "imperial_mi0_5": "У межах 0,5 милі", + "imperial_mi0_2": "У межах 0,2 милі", + "imperial_ft600": "У межах 600 фт", + "imperial_ft300": "У межах 300 фт", + "imperial_ft150": "У межах 150 фт" + } + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/commandPalette.json b/packages/web/public/i18n/locales/uk-UA/commandPalette.json new file mode 100644 index 00000000..c04c4fa5 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "Повідомлення", + "map": "Мапа", + "config": "Config", + "channels": "Канали", + "nodes": "Вузли" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR-код", + "qrGenerator": "Генератор", + "qrImport": "Import", + "scheduleShutdown": "Розклад вимкнення", + "scheduleReboot": "Розклад перезавантаження", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "Debug", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/common.json b/packages/web/public/i18n/locales/uk-UA/common.json new file mode 100644 index 00000000..02667ac1 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "Застосувати", + "backupKey": "Backup Key", + "cancel": "Скасувати", + "clearMessages": "Clear Messages", + "close": "Закрити", + "confirm": "Confirm", + "delete": "Видалити", + "dismiss": "Dismiss", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "Import", + "message": "Повідомлення", + "now": "Now", + "ok": "Гаразд", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "Видалити", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "Скинути", + "save": "Зберегти", + "scanQr": "Scan QR Code", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Завантаження...", + "unit": { + "cps": "CPS", + "dbm": "дБм", + "hertz": "Гц", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "МГц", + "raw": "raw", + "meter": { + "one": "Метр", + "plural": "Meters", + "suffix": "м" + }, + "minute": { + "one": "Minute", + "plural": "Хвилин" + }, + "hour": { + "one": "Година", + "plural": "Годин" + }, + "millisecond": { + "one": "Мілісекунда", + "plural": "Мілісекунди", + "suffix": "мс" + }, + "second": { + "one": "Секунда", + "plural": "Секунд" + }, + "day": { + "one": "День", + "plural": "Днів" + }, + "month": { + "one": "Місяць", + "plural": "Місяців" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Вольт", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "Empty", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "Unknown", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Невірний формат, потрібна IPv4-адреса.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Ключ повинен бути пустим.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "Це поле є обов'язковим.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/dashboard.json b/packages/web/public/i18n/locales/uk-UA/dashboard.json new file mode 100644 index 00000000..4fad3e69 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "Серійний порт", + "connectionType_network": "Мережа", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/deviceConfig.json b/packages/web/public/i18n/locales/uk-UA/deviceConfig.json new file mode 100644 index 00000000..0de85c36 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Налаштування", + "tabBluetooth": "Bluetooth", + "tabDevice": "Пристрій", + "tabDisplay": "Дисплей", + "tabLora": "LoRa", + "tabNetwork": "Мережа", + "tabPosition": "Position", + "tabPower": "Живлення", + "tabSecurity": "Безпека" + }, + "sidebar": { + "label": "Модулі" + }, + "device": { + "title": "Налаштування пристрою", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX Timezone" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "Rebroadcast Mode" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "Role" + } + }, + "bluetooth": { + "title": "Налаштування Bluetooth", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Увімкнути або вимкнути Bluetooth", + "label": "Увімкнено" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "Pairing mode" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "Display Mode" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "Display Units" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED Type" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "Bandwidth" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "Coding Rate" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "Ігнорувати MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "Modem Preset" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "OK to MQTT" + }, + "overrideDutyCycle": { + "description": "Override Duty Cycle", + "label": "Override Duty Cycle" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "Регіон" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "Transmit Enabled" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "Use Preset" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "Налаштування WiFi", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS-сервер", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "Enabled" + }, + "gateway": { + "description": "Default Gateway", + "label": "Шлюз" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Маска підмережі", + "label": "Підмережа" + }, + "wifiEnabled": { + "description": "Увімкнути або вимкнути WiFi радіо", + "label": "Enabled" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP-сервер" + }, + "rsyslogServer": { + "label": "Rsyslog-сервер" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "Налаштування NTP", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Налаштування Rsyslog" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "Налаштування UDP" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "Broadcast Interval" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Висота", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "Timestamp", + "unset": "Скинути", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "Enable power saving mode" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "Налаштування живлення" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Налаштування режиму сну" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Налаштування безпеки", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "Private Key" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "Public Key" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "Primary Admin Key" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "Secondary Admin Key" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "Tertiary Admin Key" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/dialog.json b/packages/web/public/i18n/locales/uk-UA/dialog.json new file mode 100644 index 00000000..20bdd125 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Очистити всі повідомлення" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "Довга назва", + "shortName": "Коротка назва", + "title": "Змінити назву пристрою" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Канал: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Канали:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Висота: ", + "coordinates": "Координати: ", + "title": "Розташування: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Перегенерувати" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "Bluetooth", + "tabSerial": "Serial", + "useHttps": "Використовувати HTTPS", + "connecting": "Підключення...", + "connect": "Connect", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "Новий пристрій" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "Повідомлення", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "Voltage", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "Згенерувати QR-код" + }, + "rebootOta": { + "title": "Запланувати перезавантаження", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Введіть затримку (сек)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Запланувати перезавантаження", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "Ви впевнені?" + }, + "managedMode": { + "confirmUnderstanding": "Я знаю, що роблю", + "title": "Ви впевнені?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/messages.json b/packages/web/public/i18n/locales/uk-UA/messages.json new file mode 100644 index 00000000..0500adb8 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Повідомлення: {{chatName}}", + "placeholder": "Введіть повідомлення" + }, + "emptyState": { + "title": "Оберіть чат", + "text": "Поки немає повідомлень." + }, + "selectChatPrompt": { + "text": "Виберіть канал або вузол для початку обміну повідомленнями." + }, + "sendMessage": { + "placeholder": "Введіть ваше повідомлення тут...", + "sendButton": "Надіслати" + }, + "actionsMenu": { + "addReactionLabel": "Додати реакцію", + "replyLabel": "Відповісти" + }, + "deliveryStatus": { + "delivered": { + "label": "Повідомлення доставлено", + "displayText": "Повідомлення доставлено" + }, + "failed": { + "label": "Помилка доставлення повідомлення", + "displayText": "Надсилання не відбулося" + }, + "unknown": { + "label": "Статус повідомлення невідомий", + "displayText": "Невідомий стан" + }, + "waiting": { + "label": "Надсилання повідомлення", + "displayText": "Очікування доставки" + } + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/moduleConfig.json b/packages/web/public/i18n/locales/uk-UA/moduleConfig.json new file mode 100644 index 00000000..89074e3d --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "Ambient Lighting", + "tabAudio": "Аудіо", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "Detection Sensor", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "Neighbor Info", + "tabPaxcounter": "Paxcounter", + "tabRangeTest": "Тест дальності", + "tabSerial": "Serial", + "tabStoreAndForward": "S&F", + "tabTelemetry": "Телеметрія" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED State", + "description": "Sets LED to on or off" + }, + "current": { + "label": "Current", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "Червоний", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "Зелений", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "Синій", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "Send Bell", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "Send Bell", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "Налаштування MQTT", + "description": "Settings for the MQTT module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "Ім'я користувача MQTT", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "Пароль MQTT", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "Encryption Enabled", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "JSON Enabled", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "TLS Enabled", + "description": "Enable or disable TLS" + }, + "root": { + "label": "Root topic", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "Enabled", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "Echo", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "Timeout", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "Mode", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "Number of records", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "History return max", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "History return window", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "Device Metrics", + "description": "Device metrics update interval (seconds)" + }, + "environmentUpdateInterval": { + "label": "Environment metrics update interval (seconds)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "Display Fahrenheit", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/nodes.json b/packages/web/public/i18n/locales/uk-UA/nodes.json new file mode 100644 index 00000000..f0ca01bc --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "Favorite", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "Помилка", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "Heard", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "Довга назва", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Шифрування", + "model": "Model", + "macAddress": "MAC-адреса" + }, + "connectionStatus": { + "direct": "Direct", + "away": "away", + "unknown": "-", + "viaMqtt": ", через MQTT" + }, + "lastHeardStatus": { + "never": "Ніколи" + } + }, + "actions": { + "added": "Додано", + "removed": "Видалено", + "ignoreNode": "Ігнорувати вузол", + "unignoreNode": "Unignore Node", + "requestPosition": "Запитати позицію" + } +} diff --git a/packages/web/public/i18n/locales/uk-UA/ui.json b/packages/web/public/i18n/locales/uk-UA/ui.json new file mode 100644 index 00000000..92c8b4b3 --- /dev/null +++ b/packages/web/public/i18n/locales/uk-UA/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Навігація", + "messages": "Повідомлення", + "map": "Мапа", + "config": "Config", + "radioConfig": "Налаштування радіо", + "moduleConfig": "Module Config", + "channels": "Канали", + "nodes": "Вузли" + }, + "app": { + "title": "Meshtastic", + "logo": "Логотип Meshtastic" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Відкрити бічну панель", + "close": "Закрити бічну панель" + } + }, + "deviceInfo": { + "volts": "{{voltage}} Вольт", + "firmware": { + "title": "Прошивка", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "Батарея" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Додано", + "removed": "Видалено", + "to": "до", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Додано", + "removed": "Видалено", + "to": "до", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Скопійовано!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "Hide password" + }, + "showPassword": { + "label": "Show password" + }, + "deliveryStatus": { + "delivered": "Доставлено", + "failed": "Delivery Failed", + "waiting": "Очікування", + "unknown": "Unknown" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Метрики" + }, + "role": { + "label": "Role" + }, + "filter": { + "label": "Фільтри" + }, + "advanced": { + "label": "Розширені" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Напруга" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Direct", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Last heard", + "labelText": "Last heard: {{value}}", + "nowLabel": "Зараз" + }, + "snr": { + "label": "SNR (дБ)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Сховати" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "Мова", + "changeLanguage": "Змінити мову" + }, + "theme": { + "dark": "Темна", + "light": "Світла", + "system": "Automatic", + "changeTheme": "Змінити схему кольорів" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Інформація про помилку", + "errorMessageLabel": "Помилка:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/channels.json b/packages/web/public/i18n/locales/zh-CN/channels.json new file mode 100644 index 00000000..3ad45930 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/channels.json @@ -0,0 +1,69 @@ +{ + "page": { + "sectionLabel": "频道", + "channelName": "Channel: {{channelName}}", + "broadcastLabel": "主要", + "channelIndex": "Ch {{index}}" + }, + "validation": { + "pskInvalid": "Please enter a valid {{bits}} bit PSK." + }, + "settings": { + "label": "频道设置", + "description": "Crypto, MQTT & misc settings" + }, + "role": { + "label": "角色", + "description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed", + "options": { + "primary": "PRIMARY", + "disabled": "DISABLED", + "secondary": "SECONDARY" + } + }, + "psk": { + "label": "Pre-Shared Key", + "description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)", + "generate": "Generate" + }, + "name": { + "label": "名称", + "description": "A unique name for the channel <12 bytes, leave blank for default" + }, + "uplinkEnabled": { + "label": "启用上传", + "description": "Send messages from the local mesh to MQTT" + }, + "downlinkEnabled": { + "label": "启用下载", + "description": "Send messages from MQTT to the local mesh" + }, + "positionPrecision": { + "label": "位置", + "description": "The precision of the location to share with the channel. Can be disabled.", + "options": { + "none": "Do not share location", + "precise": "Precise Location", + "metric_km23": "Within 23 kilometers", + "metric_km12": "Within 12 kilometers", + "metric_km5_8": "Within 5.8 kilometers", + "metric_km2_9": "Within 2.9 kilometers", + "metric_km1_5": "Within 1.5 kilometers", + "metric_m700": "Within 700 meters", + "metric_m350": "Within 350 meters", + "metric_m200": "Within 200 meters", + "metric_m90": "Within 90 meters", + "metric_m50": "Within 50 meters", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/commandPalette.json b/packages/web/public/i18n/locales/zh-CN/commandPalette.json new file mode 100644 index 00000000..685ec409 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/commandPalette.json @@ -0,0 +1,50 @@ +{ + "emptyState": "No results found.", + "page": { + "title": "Command Menu" + }, + "pinGroup": { + "label": "Pin command group" + }, + "unpinGroup": { + "label": "Unpin command group" + }, + "goto": { + "label": "Goto", + "command": { + "messages": "消息", + "map": "地图", + "config": "配置", + "channels": "频道", + "nodes": "节点" + } + }, + "manage": { + "label": "Manage", + "command": { + "switchNode": "Switch Node", + "connectNewNode": "Connect New Node" + } + }, + "contextual": { + "label": "Contextual", + "command": { + "qrCode": "QR Code", + "qrGenerator": "Generator", + "qrImport": "导入", + "scheduleShutdown": "Schedule Shutdown", + "scheduleReboot": "Schedule Reboot", + "rebootToOtaMode": "Reboot To OTA Mode", + "resetNodeDb": "Reset Node DB", + "factoryResetDevice": "Factory Reset Device", + "factoryResetConfig": "Factory Reset Config" + } + }, + "debug": { + "label": "调试", + "command": { + "reconfigure": "Reconfigure", + "clearAllStoredMessages": "Clear All Stored Message" + } + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/common.json b/packages/web/public/i18n/locales/zh-CN/common.json new file mode 100644 index 00000000..0498cc54 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/common.json @@ -0,0 +1,141 @@ +{ + "button": { + "apply": "申请", + "backupKey": "Backup Key", + "cancel": "取消", + "clearMessages": "Clear Messages", + "close": "关闭", + "confirm": "Confirm", + "delete": "删除", + "dismiss": "收起键盘", + "download": "Download", + "export": "Export", + "generate": "Generate", + "regenerate": "Regenerate", + "import": "导入", + "message": "信息", + "now": "Now", + "ok": "好的", + "print": "Print", + "rebootOtaNow": "Reboot to OTA Mode Now", + "remove": "移除", + "requestNewKeys": "Request New Keys", + "requestPosition": "Request Position", + "reset": "重置", + "save": "保存", + "scanQr": "扫描二维码", + "traceRoute": "Trace Route", + "submit": "Submit" + }, + "app": { + "title": "Meshtastic", + "fullTitle": "Meshtastic Web Client" + }, + "loading": "Loading...", + "unit": { + "cps": "CPS", + "dbm": "dBm", + "hertz": "Hz", + "hop": { + "one": "Hop", + "plural": "Hops" + }, + "hopsAway": { + "one": "{{count}} hop away", + "plural": "{{count}} hops away", + "unknown": "Unknown hops away" + }, + "megahertz": "MHz", + "raw": "raw", + "meter": { + "one": "Meter", + "plural": "Meters", + "suffix": "m" + }, + "minute": { + "one": "Minute", + "plural": "Minutes" + }, + "hour": { + "one": "小时", + "plural": "Hours" + }, + "millisecond": { + "one": "Millisecond", + "plural": "Milliseconds", + "suffix": "ms" + }, + "second": { + "one": "Second", + "plural": "Seconds" + }, + "day": { + "one": "Day", + "plural": "Days" + }, + "month": { + "one": "Month", + "plural": "Months" + }, + "year": { + "one": "Year", + "plural": "Years" + }, + "snr": "SNR", + "volt": { + "one": "Volt", + "plural": "Volts", + "suffix": "V" + }, + "record": { + "one": "Records", + "plural": "Records" + } + }, + "security": { + "0bit": "空", + "8bit": "8 bit", + "128bit": "128 bit", + "256bit": "256 bit" + }, + "unknown": { + "longName": "未知", + "shortName": "UNK", + "notAvailable": "N/A", + "num": "??" + }, + "nodeUnknownPrefix": "!", + "unset": "UNSET", + "fallbackName": "Meshtastic {{last4}}", + "node": "Node", + "formValidation": { + "unsavedChanges": "Unsaved changes", + "tooBig": { + "string": "Too long, expected less than or equal to {{maximum}} characters.", + "number": "Too big, expected a number smaller than or equal to {{maximum}}.", + "bytes": "Too big, expected less than or equal to {{params.maximum}} bytes." + }, + "tooSmall": { + "string": "Too short, expected more than or equal to {{minimum}} characters.", + "number": "Too small, expected a number larger than or equal to {{minimum}}." + }, + "invalidFormat": { + "ipv4": "Invalid format, expected an IPv4 address.", + "key": "Invalid format, expected a Base64 encoded pre-shared key (PSK)." + }, + "invalidType": { + "number": "Invalid type, expected a number." + }, + "pskLength": { + "0bit": "Key is required to be empty.", + "8bit": "Key is required to be an 8 bit pre-shared key (PSK).", + "128bit": "Key is required to be a 128 bit pre-shared key (PSK).", + "256bit": "Key is required to be a 256 bit pre-shared key (PSK)." + }, + "required": { + "generic": "This field is required.", + "managed": "At least one admin key is requred if the node is managed.", + "key": "Key is required." + } + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/dashboard.json b/packages/web/public/i18n/locales/zh-CN/dashboard.json new file mode 100644 index 00000000..a7b78399 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/dashboard.json @@ -0,0 +1,12 @@ +{ + "dashboard": { + "title": "Connected Devices", + "description": "Manage your connected Meshtastic devices.", + "connectionType_ble": "BLE", + "connectionType_serial": "串口", + "connectionType_network": "网络", + "noDevicesTitle": "No devices connected", + "noDevicesDescription": "Connect a new device to get started.", + "button_newConnection": "New Connection" + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/deviceConfig.json b/packages/web/public/i18n/locales/zh-CN/deviceConfig.json new file mode 100644 index 00000000..590aeb25 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/deviceConfig.json @@ -0,0 +1,428 @@ +{ + "page": { + "title": "Configuration", + "tabBluetooth": "蓝牙", + "tabDevice": "设备", + "tabDisplay": "显示", + "tabLora": "LoRa", + "tabNetwork": "网络", + "tabPosition": "定位", + "tabPower": "电源", + "tabSecurity": "安全" + }, + "sidebar": { + "label": "Modules" + }, + "device": { + "title": "Device Settings", + "description": "Settings for the device", + "buttonPin": { + "description": "Button pin override", + "label": "Button Pin" + }, + "buzzerPin": { + "description": "Buzzer pin override", + "label": "Buzzer Pin" + }, + "disableTripleClick": { + "description": "Disable triple click", + "label": "Disable Triple Click" + }, + "doubleTapAsButtonPress": { + "description": "Treat double tap as button press", + "label": "Double Tap as Button Press" + }, + "ledHeartbeatDisabled": { + "description": "Disable default blinking LED", + "label": "LED Heartbeat Disabled" + }, + "nodeInfoBroadcastInterval": { + "description": "How often to broadcast node info", + "label": "Node Info Broadcast Interval" + }, + "posixTimezone": { + "description": "The POSIX timezone string for the device", + "label": "POSIX 时区" + }, + "rebroadcastMode": { + "description": "How to handle rebroadcasting", + "label": "转播模式" + }, + "role": { + "description": "What role the device performs on the mesh", + "label": "角色" + } + }, + "bluetooth": { + "title": "Bluetooth Settings", + "description": "Settings for the Bluetooth module", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "enabled": { + "description": "Enable or disable Bluetooth", + "label": "启用" + }, + "pairingMode": { + "description": "Pin selection behaviour.", + "label": "配对模式" + }, + "pin": { + "description": "Pin to use when pairing", + "label": "Pin" + } + }, + "display": { + "description": "Settings for the device display", + "title": "Display Settings", + "headingBold": { + "description": "Bolden the heading text", + "label": "Bold Heading" + }, + "carouselDelay": { + "description": "How fast to cycle through windows", + "label": "Carousel Delay" + }, + "compassNorthTop": { + "description": "Fix north to the top of compass", + "label": "Compass North Top" + }, + "displayMode": { + "description": "Screen layout variant", + "label": "显示模式" + }, + "displayUnits": { + "description": "Display metric or imperial units", + "label": "显示单位" + }, + "flipScreen": { + "description": "Flip display 180 degrees", + "label": "Flip Screen" + }, + "gpsDisplayUnits": { + "description": "Coordinate display format", + "label": "GPS Display Units" + }, + "oledType": { + "description": "Type of OLED screen attached to the device", + "label": "OLED 类型" + }, + "screenTimeout": { + "description": "Turn off the display after this long", + "label": "Screen Timeout" + }, + "twelveHourClock": { + "description": "Use 12-hour clock format", + "label": "12-Hour Clock" + }, + "wakeOnTapOrMotion": { + "description": "Wake the device on tap or motion", + "label": "Wake on Tap or Motion" + } + }, + "lora": { + "title": "Mesh Settings", + "description": "Settings for the LoRa mesh", + "bandwidth": { + "description": "Channel bandwidth in MHz", + "label": "带宽" + }, + "boostedRxGain": { + "description": "Boosted RX gain", + "label": "Boosted RX Gain" + }, + "codingRate": { + "description": "The denominator of the coding rate", + "label": "编码率" + }, + "frequencyOffset": { + "description": "Frequency offset to correct for crystal calibration errors", + "label": "Frequency Offset" + }, + "frequencySlot": { + "description": "LoRa frequency channel number", + "label": "Frequency Slot" + }, + "hopLimit": { + "description": "Maximum number of hops", + "label": "Hop Limit" + }, + "ignoreMqtt": { + "description": "Don't forward MQTT messages over the mesh", + "label": "忽略 MQTT" + }, + "modemPreset": { + "description": "Modem preset to use", + "label": "调制解调器预设" + }, + "okToMqtt": { + "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", + "label": "使用MQTT" + }, + "overrideDutyCycle": { + "description": "覆盖占空比", + "label": "覆盖占空比" + }, + "overrideFrequency": { + "description": "Override frequency", + "label": "Override Frequency" + }, + "region": { + "description": "Sets the region for your node", + "label": "区域" + }, + "spreadingFactor": { + "description": "Indicates the number of chirps per symbol", + "label": "Spreading Factor" + }, + "transmitEnabled": { + "description": "Enable/Disable transmit (TX) from the LoRa radio", + "label": "启用传输" + }, + "transmitPower": { + "description": "Max transmit power", + "label": "Transmit Power" + }, + "usePreset": { + "description": "Use one of the predefined modem presets", + "label": "使用预设" + }, + "meshSettings": { + "description": "Settings for the LoRa mesh", + "label": "Mesh Settings" + }, + "waveformSettings": { + "description": "Settings for the LoRa waveform", + "label": "Waveform Settings" + }, + "radioSettings": { + "label": "Radio Settings", + "description": "Settings for the LoRa radio" + } + }, + "network": { + "title": "WiFi Config", + "description": "WiFi radio configuration", + "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", + "addressMode": { + "description": "Address assignment selection", + "label": "Address Mode" + }, + "dns": { + "description": "DNS Server", + "label": "DNS" + }, + "ethernetEnabled": { + "description": "Enable or disable the Ethernet port", + "label": "启用" + }, + "gateway": { + "description": "Default Gateway", + "label": "网关" + }, + "ip": { + "description": "IP Address", + "label": "IP" + }, + "psk": { + "description": "Network password", + "label": "共享密钥/PSK" + }, + "ssid": { + "description": "Network name", + "label": "SSID" + }, + "subnet": { + "description": "Subnet Mask", + "label": "子网" + }, + "wifiEnabled": { + "description": "Enable or disable the WiFi radio", + "label": "启用" + }, + "meshViaUdp": { + "label": "Mesh via UDP" + }, + "ntpServer": { + "label": "NTP Server" + }, + "rsyslogServer": { + "label": "Rsyslog Server" + }, + "ethernetConfigSettings": { + "description": "Ethernet port configuration", + "label": "Ethernet Config" + }, + "ipConfigSettings": { + "description": "IP configuration", + "label": "IP Config" + }, + "ntpConfigSettings": { + "description": "NTP configuration", + "label": "NTP Config" + }, + "rsyslogConfigSettings": { + "description": "Rsyslog configuration", + "label": "Rsyslog Config" + }, + "udpConfigSettings": { + "description": "UDP over Mesh configuration", + "label": "UDP 设置" + } + }, + "position": { + "title": "Position Settings", + "description": "Settings for the position module", + "broadcastInterval": { + "description": "How often your position is sent out over the mesh", + "label": "广播间隔" + }, + "enablePin": { + "description": "GPS module enable pin override", + "label": "Enable Pin" + }, + "fixedPosition": { + "description": "Don't report GPS position, but a manually-specified one", + "label": "Fixed Position" + }, + "gpsMode": { + "description": "Configure whether device GPS is Enabled, Disabled, or Not Present", + "label": "GPS Mode" + }, + "gpsUpdateInterval": { + "description": "How often a GPS fix should be acquired", + "label": "GPS Update Interval" + }, + "positionFlags": { + "description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", + "label": "Position Flags" + }, + "receivePin": { + "description": "GPS module RX pin override", + "label": "Receive Pin" + }, + "smartPositionEnabled": { + "description": "Only send position when there has been a meaningful change in location", + "label": "Enable Smart Position" + }, + "smartPositionMinDistance": { + "description": "Minimum distance (in meters) that must be traveled before a position update is sent", + "label": "Smart Position Minimum Distance" + }, + "smartPositionMinInterval": { + "description": "Minimum interval (in seconds) that must pass before a position update is sent", + "label": "Smart Position Minimum Interval" + }, + "transmitPin": { + "description": "GPS module TX pin override", + "label": "Transmit Pin" + }, + "intervalsSettings": { + "description": "How often to send position updates", + "label": "Intervals" + }, + "flags": { + "placeholder": "Select position flags...", + "altitude": "Altitude", + "altitudeGeoidalSeparation": "Altitude Geoidal Separation", + "altitudeMsl": "Altitude is Mean Sea Level", + "dop": "Dilution of precision (DOP) PDOP used by default", + "hdopVdop": "如果设置了 DOP,则使用 HDOP / VDOP 值而不是 PDOP", + "numSatellites": "Number of satellites", + "sequenceNumber": "Sequence number", + "timestamp": "时间戳", + "unset": "未设置", + "vehicleHeading": "Vehicle heading", + "vehicleSpeed": "Vehicle speed" + } + }, + "power": { + "adcMultiplierOverride": { + "description": "Used for tweaking battery voltage reading", + "label": "ADC Multiplier Override ratio" + }, + "ina219Address": { + "description": "Address of the INA219 battery monitor", + "label": "INA219 Address" + }, + "lightSleepDuration": { + "description": "How long the device will be in light sleep for", + "label": "Light Sleep Duration" + }, + "minimumWakeTime": { + "description": "Minimum amount of time the device will stay awake for after receiving a packet", + "label": "Minimum Wake Time" + }, + "noConnectionBluetoothDisabled": { + "description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long", + "label": "No Connection Bluetooth Disabled" + }, + "powerSavingEnabled": { + "description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.", + "label": "启用节能模式" + }, + "shutdownOnBatteryDelay": { + "description": "Automatically shutdown node after this long when on battery, 0 for indefinite", + "label": "Shutdown on battery delay" + }, + "superDeepSleepDuration": { + "description": "How long the device will be in super deep sleep for", + "label": "Super Deep Sleep Duration" + }, + "powerConfigSettings": { + "description": "Settings for the power module", + "label": "电源配置" + }, + "sleepSettings": { + "description": "Sleep settings for the power module", + "label": "Sleep Settings" + } + }, + "security": { + "description": "Settings for the Security configuration", + "title": "Security Settings", + "button_backupKey": "Backup Key", + "adminChannelEnabled": { + "description": "Allow incoming device control over the insecure legacy admin channel", + "label": "Allow Legacy Admin" + }, + "enableDebugLogApi": { + "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", + "label": "Enable Debug Log API" + }, + "managed": { + "description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.", + "label": "Managed" + }, + "privateKey": { + "description": "Used to create a shared key with a remote device", + "label": "私钥" + }, + "publicKey": { + "description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key", + "label": "公钥" + }, + "primaryAdminKey": { + "description": "The primary public key authorized to send admin messages to this node", + "label": "一级管理员密钥" + }, + "secondaryAdminKey": { + "description": "The secondary public key authorized to send admin messages to this node", + "label": "二级管理员密钥" + }, + "serialOutputEnabled": { + "description": "Serial Console over the Stream API", + "label": "Serial Output Enabled" + }, + "tertiaryAdminKey": { + "description": "The tertiary public key authorized to send admin messages to this node", + "label": "三级管理员密钥" + }, + "adminSettings": { + "description": "Settings for Admin", + "label": "Admin Settings" + }, + "loggingSettings": { + "description": "Settings for Logging", + "label": "Logging Settings" + } + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/dialog.json b/packages/web/public/i18n/locales/zh-CN/dialog.json new file mode 100644 index 00000000..f3203783 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/dialog.json @@ -0,0 +1,171 @@ +{ + "deleteMessages": { + "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", + "title": "Clear All Messages" + }, + "deviceName": { + "description": "The Device will restart once the config is saved.", + "longName": "长名称", + "shortName": "短名称", + "title": "Change Device Name" + }, + "import": { + "description": "The current LoRa configuration will be overridden.", + "error": { + "invalidUrl": "Invalid Meshtastic URL" + }, + "channelPrefix": "Channel: ", + "channelSetUrl": "Channel Set/QR Code URL", + "channels": "Channels:", + "usePreset": "Use Preset?", + "title": "Import Channel Set" + }, + "locationResponse": { + "altitude": "Altitude: ", + "coordinates": "Coordinates: ", + "title": "Location: {{identifier}}" + }, + "pkiRegenerateDialog": { + "title": "Regenerate Pre-Shared Key?", + "description": "Are you sure you want to regenerate the pre-shared key?", + "regenerate": "Regenerate" + }, + "newDeviceDialog": { + "title": "Connect New Device", + "https": "https", + "http": "http", + "tabHttp": "HTTP", + "tabBluetooth": "蓝牙", + "tabSerial": "串口", + "useHttps": "Use HTTPS", + "connecting": "Connecting...", + "connect": "连接", + "connectionFailedAlert": { + "title": "Connection Failed", + "descriptionPrefix": "Could not connect to the device. ", + "httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ", + "openLinkPrefix": "Please open ", + "openLinkSuffix": " in a new tab", + "acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again", + "learnMoreLink": "Learn more" + }, + "httpConnection": { + "label": "IP Address/Hostname", + "placeholder": "000.000.000.000 / meshtastic.local" + }, + "serialConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device", + "deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}" + }, + "bluetoothConnection": { + "noDevicesPaired": "No devices paired yet.", + "newDeviceButton": "New device" + }, + "validation": { + "requiresWebBluetooth": "This connection type requires <0>Web Bluetooth. Please use a supported browser, like Chrome or Edge.", + "requiresWebSerial": "This connection type requires <0>Web Serial. Please use a supported browser, like Chrome or Edge.", + "requiresSecureContext": "This application requires a <0>secure context. Please connect using HTTPS or localhost.", + "additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context. Please connect using HTTPS or localhost." + } + }, + "nodeDetails": { + "message": "信息", + "requestPosition": "Request Position", + "traceRoute": "Trace Route", + "airTxUtilization": "Air TX utilization", + "allRawMetrics": "All Raw Metrics:", + "batteryLevel": "Battery level", + "channelUtilization": "Channel utilization", + "details": "Details:", + "deviceMetrics": "Device Metrics:", + "hardware": "Hardware: ", + "lastHeard": "Last Heard: ", + "nodeHexPrefix": "Node Hex: !", + "nodeNumber": "Node Number: ", + "position": "Position:", + "role": "Role: ", + "uptime": "Uptime: ", + "voltage": "电压", + "title": "Node Details for {{identifier}}", + "ignoreNode": "Ignore node", + "removeNode": "Remove node", + "unignoreNode": "Unignore node" + }, + "pkiBackup": { + "loseKeysWarning": "If you lose your keys, you will need to reset your device.", + "secureBackup": "Its important to backup your public and private keys and store your backup securely!", + "footer": "=== END OF KEYS ===", + "header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===", + "privateKey": "Private Key:", + "publicKey": "Public Key:", + "fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt", + "title": "Backup Keys" + }, + "pkiBackupReminder": { + "description": "We recommend backing up your key data regularly. Would you like to back up now?", + "title": "Backup Reminder", + "remindLaterPrefix": "Remind me in", + "remindNever": "Never remind me", + "backupNow": "Back up now" + }, + "pkiRegenerate": { + "description": "Are you sure you want to regenerate key pair?", + "title": "Regenerate Key Pair" + }, + "qr": { + "addChannels": "Add Channels", + "replaceChannels": "Replace Channels", + "description": "The current LoRa configuration will also be shared.", + "sharableUrl": "Sharable URL", + "title": "生成二维码" + }, + "rebootOta": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.", + "enterDelay": "Enter delay (sec)", + "scheduled": "Reboot has been scheduled" + }, + "reboot": { + "title": "Schedule Reboot", + "description": "Reboot the connected node after x minutes." + }, + "refreshKeys": { + "description": { + "acceptNewKeys": "This will remove the node from device and request new keys.", + "keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.", + "unableToSendDmPrefix": "Your node is unable to send a direct message to node: " + }, + "acceptNewKeys": "Accept New Keys", + "title": "Keys Mismatch - {{identifier}}" + }, + "removeNode": { + "description": "Are you sure you want to remove this Node?", + "title": "Remove Node?" + }, + "shutdown": { + "title": "Schedule Shutdown", + "description": "Turn off the connected node after x minutes." + }, + "traceRoute": { + "routeToDestination": "Route to destination:", + "routeBack": "Route back:" + }, + "tracerouteResponse": { + "title": "Traceroute: {{identifier}}" + }, + "unsafeRoles": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "conjunction": " and the blog post about ", + "postamble": " and understand the implications of changing the role.", + "preamble": "I have read the ", + "choosingRightDeviceRole": "Choosing The Right Device Role", + "deviceRoleDocumentation": "Device Role Documentation", + "title": "是否确认?" + }, + "managedMode": { + "confirmUnderstanding": "Yes, I know what I'm doing", + "title": "是否确认?", + "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/messages.json b/packages/web/public/i18n/locales/zh-CN/messages.json new file mode 100644 index 00000000..35f3380b --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/messages.json @@ -0,0 +1,39 @@ +{ + "page": { + "title": "Messages: {{chatName}}", + "placeholder": "Enter Message" + }, + "emptyState": { + "title": "Select a Chat", + "text": "No messages yet." + }, + "selectChatPrompt": { + "text": "Select a channel or node to start messaging." + }, + "sendMessage": { + "placeholder": "Enter your message here...", + "sendButton": "传送" + }, + "actionsMenu": { + "addReactionLabel": "Add Reaction", + "replyLabel": "回复" + }, + "deliveryStatus": { + "delivered": { + "label": "Message delivered", + "displayText": "Message delivered" + }, + "failed": { + "label": "Message delivery failed", + "displayText": "Delivery failed" + }, + "unknown": { + "label": "Message status unknown", + "displayText": "Unknown state" + }, + "waiting": { + "label": "Sending message", + "displayText": "Waiting for delivery" + } + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/moduleConfig.json b/packages/web/public/i18n/locales/zh-CN/moduleConfig.json new file mode 100644 index 00000000..4bcce7d2 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/moduleConfig.json @@ -0,0 +1,448 @@ +{ + "page": { + "tabAmbientLighting": "氛围灯", + "tabAudio": "音频", + "tabCannedMessage": "Canned", + "tabDetectionSensor": "检测传感器", + "tabExternalNotification": "Ext Notif", + "tabMqtt": "MQTT", + "tabNeighborInfo": "邻居信息", + "tabPaxcounter": "客流计数", + "tabRangeTest": "拉距测试", + "tabSerial": "串口", + "tabStoreAndForward": "S&F", + "tabTelemetry": "遥测(传感器)" + }, + "ambientLighting": { + "title": "Ambient Lighting Settings", + "description": "Settings for the Ambient Lighting module", + "ledState": { + "label": "LED 状态", + "description": "Sets LED to on or off" + }, + "current": { + "label": "电流", + "description": "Sets the current for the LED output. Default is 10" + }, + "red": { + "label": "红", + "description": "Sets the red LED level. Values are 0-255" + }, + "green": { + "label": "绿", + "description": "Sets the green LED level. Values are 0-255" + }, + "blue": { + "label": "蓝", + "description": "Sets the blue LED level. Values are 0-255" + } + }, + "audio": { + "title": "Audio Settings", + "description": "Settings for the Audio module", + "codec2Enabled": { + "label": "Codec 2 Enabled", + "description": "Enable Codec 2 audio encoding" + }, + "pttPin": { + "label": "PTT Pin", + "description": "GPIO pin to use for PTT" + }, + "bitrate": { + "label": "Bitrate", + "description": "Bitrate to use for audio encoding" + }, + "i2sWs": { + "label": "i2S WS", + "description": "GPIO pin to use for i2S WS" + }, + "i2sSd": { + "label": "i2S SD", + "description": "GPIO pin to use for i2S SD" + }, + "i2sDin": { + "label": "i2S DIN", + "description": "GPIO pin to use for i2S DIN" + }, + "i2sSck": { + "label": "i2S SCK", + "description": "GPIO pin to use for i2S SCK" + } + }, + "cannedMessage": { + "title": "Canned Message Settings", + "description": "Settings for the Canned Message module", + "moduleEnabled": { + "label": "Module Enabled", + "description": "Enable Canned Message" + }, + "rotary1Enabled": { + "label": "Rotary Encoder #1 Enabled", + "description": "Enable the rotary encoder" + }, + "inputbrokerPinA": { + "label": "Encoder Pin A", + "description": "GPIO Pin Value (1-39) For encoder port A" + }, + "inputbrokerPinB": { + "label": "Encoder Pin B", + "description": "GPIO Pin Value (1-39) For encoder port B" + }, + "inputbrokerPinPress": { + "label": "Encoder Pin Press", + "description": "GPIO Pin Value (1-39) For encoder Press" + }, + "inputbrokerEventCw": { + "label": "Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventCcw": { + "label": "Counter Clockwise event", + "description": "Select input event." + }, + "inputbrokerEventPress": { + "label": "Press event", + "description": "Select input event" + }, + "updown1Enabled": { + "label": "Up Down enabled", + "description": "Enable the up / down encoder" + }, + "allowInputSource": { + "label": "Allow Input Source", + "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" + }, + "sendBell": { + "label": "发送铃声", + "description": "Sends a bell character with each message" + } + }, + "detectionSensor": { + "title": "Detection Sensor Settings", + "description": "Settings for the Detection Sensor module", + "enabled": { + "label": "启用", + "description": "Enable or disable Detection Sensor Module" + }, + "minimumBroadcastSecs": { + "label": "Minimum Broadcast Seconds", + "description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" + }, + "stateBroadcastSecs": { + "label": "State Broadcast Seconds", + "description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" + }, + "sendBell": { + "label": "发送铃声", + "description": "Send ASCII bell with alert message" + }, + "name": { + "label": "Friendly Name", + "description": "Used to format the message sent to mesh, max 20 Characters" + }, + "monitorPin": { + "label": "Monitor Pin", + "description": "The GPIO pin to monitor for state changes" + }, + "detectionTriggerType": { + "label": "Detection Triggered Type", + "description": "The type of trigger event to be used" + }, + "usePullup": { + "label": "Use Pullup", + "description": "Whether or not use INPUT_PULLUP mode for GPIO pin" + } + }, + "externalNotification": { + "title": "External Notification Settings", + "description": "Configure the external notification module", + "enabled": { + "label": "Module Enabled", + "description": "Enable External Notification" + }, + "outputMs": { + "label": "Output MS", + "description": "Output MS" + }, + "output": { + "label": "Output", + "description": "Output" + }, + "outputVibra": { + "label": "Output Vibrate", + "description": "Output Vibrate" + }, + "outputBuzzer": { + "label": "Output Buzzer", + "description": "Output Buzzer" + }, + "active": { + "label": "Active", + "description": "Active" + }, + "alertMessage": { + "label": "Alert Message", + "description": "Alert Message" + }, + "alertMessageVibra": { + "label": "Alert Message Vibrate", + "description": "Alert Message Vibrate" + }, + "alertMessageBuzzer": { + "label": "Alert Message Buzzer", + "description": "Alert Message Buzzer" + }, + "alertBell": { + "label": "Alert Bell", + "description": "Should an alert be triggered when receiving an incoming bell?" + }, + "alertBellVibra": { + "label": "Alert Bell Vibrate", + "description": "Alert Bell Vibrate" + }, + "alertBellBuzzer": { + "label": "Alert Bell Buzzer", + "description": "Alert Bell Buzzer" + }, + "usePwm": { + "label": "Use PWM", + "description": "Use PWM" + }, + "nagTimeout": { + "label": "Nag Timeout", + "description": "Nag Timeout" + }, + "useI2sAsBuzzer": { + "label": "Use I²S Pin as Buzzer", + "description": "Designate I²S Pin as Buzzer Output" + } + }, + "mqtt": { + "title": "MQTT Settings", + "description": "Settings for the MQTT module", + "enabled": { + "label": "启用", + "description": "Enable or disable MQTT" + }, + "address": { + "label": "MQTT Server Address", + "description": "MQTT server address to use for default/custom servers" + }, + "username": { + "label": "MQTT Username", + "description": "MQTT username to use for default/custom servers" + }, + "password": { + "label": "MQTT Password", + "description": "MQTT password to use for default/custom servers" + }, + "encryptionEnabled": { + "label": "启用加密", + "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." + }, + "jsonEnabled": { + "label": "启用 JSON", + "description": "Whether to send/consume JSON packets on MQTT" + }, + "tlsEnabled": { + "label": "启用 TLS", + "description": "Enable or disable TLS" + }, + "root": { + "label": "根主题", + "description": "MQTT root topic to use for default/custom servers" + }, + "proxyToClientEnabled": { + "label": "MQTT Client Proxy Enabled", + "description": "Utilizes the network connection to proxy MQTT messages to the client." + }, + "mapReportingEnabled": { + "label": "Map Reporting Enabled", + "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." + }, + "mapReportSettings": { + "publishIntervalSecs": { + "label": "Map Report Publish Interval (s)", + "description": "Interval in seconds to publish map reports" + }, + "positionPrecision": { + "label": "Approximate Location", + "description": "Position shared will be accurate within this distance", + "options": { + "metric_km23": "Within 23 km", + "metric_km12": "Within 12 km", + "metric_km5_8": "Within 5.8 km", + "metric_km2_9": "Within 2.9 km", + "metric_km1_5": "Within 1.5 km", + "metric_m700": "Within 700 m", + "metric_m350": "Within 350 m", + "metric_m200": "Within 200 m", + "metric_m90": "Within 90 m", + "metric_m50": "Within 50 m", + "imperial_mi15": "Within 15 miles", + "imperial_mi7_3": "Within 7.3 miles", + "imperial_mi3_6": "Within 3.6 miles", + "imperial_mi1_8": "Within 1.8 miles", + "imperial_mi0_9": "Within 0.9 miles", + "imperial_mi0_5": "Within 0.5 miles", + "imperial_mi0_2": "Within 0.2 miles", + "imperial_ft600": "Within 600 feet", + "imperial_ft300": "Within 300 feet", + "imperial_ft150": "Within 150 feet" + } + } + } + }, + "neighborInfo": { + "title": "Neighbor Info Settings", + "description": "Settings for the Neighbor Info module", + "enabled": { + "label": "启用", + "description": "Enable or disable Neighbor Info Module" + }, + "updateInterval": { + "label": "Update Interval", + "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" + } + }, + "paxcounter": { + "title": "Paxcounter Settings", + "description": "Settings for the Paxcounter module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Paxcounter" + }, + "paxcounterUpdateInterval": { + "label": "Update Interval (seconds)", + "description": "How long to wait between sending paxcounter packets" + }, + "wifiThreshold": { + "label": "WiFi RSSI Threshold", + "description": "At what WiFi RSSI level should the counter increase. Defaults to -80." + }, + "bleThreshold": { + "label": "BLE RSSI Threshold", + "description": "At what BLE RSSI level should the counter increase. Defaults to -80." + } + }, + "rangeTest": { + "title": "Range Test Settings", + "description": "Settings for the Range Test module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Range Test" + }, + "sender": { + "label": "Message Interval", + "description": "How long to wait between sending test packets" + }, + "save": { + "label": "Save CSV to storage", + "description": "ESP32 Only" + } + }, + "serial": { + "title": "Serial Settings", + "description": "Settings for the Serial module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Serial output" + }, + "echo": { + "label": "回声", + "description": "Any packets you send will be echoed back to your device" + }, + "rxd": { + "label": "Receive Pin", + "description": "Set the GPIO pin to the RXD pin you have set up." + }, + "txd": { + "label": "Transmit Pin", + "description": "Set the GPIO pin to the TXD pin you have set up." + }, + "baud": { + "label": "Baud Rate", + "description": "The serial baud rate" + }, + "timeout": { + "label": "超时", + "description": "Seconds to wait before we consider your packet as 'done'" + }, + "mode": { + "label": "模式", + "description": "Select Mode" + }, + "overrideConsoleSerialPort": { + "label": "Override Console Serial Port", + "description": "If you have a serial port connected to the console, this will override it." + } + }, + "storeForward": { + "title": "Store & Forward Settings", + "description": "Settings for the Store & Forward module", + "enabled": { + "label": "Module Enabled", + "description": "Enable Store & Forward" + }, + "heartbeat": { + "label": "Heartbeat Enabled", + "description": "Enable Store & Forward heartbeat" + }, + "records": { + "label": "记录数", + "description": "Number of records to store" + }, + "historyReturnMax": { + "label": "历史记录最大返回值", + "description": "Max number of records to return" + }, + "historyReturnWindow": { + "label": "历史记录返回窗口", + "description": "Max number of records to return" + } + }, + "telemetry": { + "title": "Telemetry Settings", + "description": "Settings for the Telemetry module", + "deviceUpdateInterval": { + "label": "设备指标", + "description": "设备计量更新间隔 (秒)" + }, + "environmentUpdateInterval": { + "label": "环境计量更新间隔 (秒)", + "description": "" + }, + "environmentMeasurementEnabled": { + "label": "Module Enabled", + "description": "Enable the Environment Telemetry" + }, + "environmentScreenEnabled": { + "label": "Displayed on Screen", + "description": "Show the Telemetry Module on the OLED" + }, + "environmentDisplayFahrenheit": { + "label": "展示华氏度", + "description": "Display temp in Fahrenheit" + }, + "airQualityEnabled": { + "label": "Air Quality Enabled", + "description": "Enable the Air Quality Telemetry" + }, + "airQualityInterval": { + "label": "Air Quality Update Interval", + "description": "How often to send Air Quality data over the mesh" + }, + "powerMeasurementEnabled": { + "label": "Power Measurement Enabled", + "description": "Enable the Power Measurement Telemetry" + }, + "powerUpdateInterval": { + "label": "Power Update Interval", + "description": "How often to send Power data over the mesh" + }, + "powerScreenEnabled": { + "label": "Power Screen Enabled", + "description": "Enable the Power Telemetry Screen" + } + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/nodes.json b/packages/web/public/i18n/locales/zh-CN/nodes.json new file mode 100644 index 00000000..f33a4313 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/nodes.json @@ -0,0 +1,63 @@ +{ + "nodeDetail": { + "publicKeyEnabled": { + "label": "Public Key Enabled" + }, + "noPublicKey": { + "label": "No Public Key" + }, + "directMessage": { + "label": "Direct Message {{shortName}}" + }, + "favorite": { + "label": "收藏", + "tooltip": "Add or remove this node from your favorites" + }, + "notFavorite": { + "label": "Not a Favorite" + }, + "error": { + "label": "错误", + "text": "An error occurred while fetching node details. Please try again later." + }, + "status": { + "heard": "收到", + "mqtt": "MQTT" + }, + "elevation": { + "label": "Elevation" + }, + "channelUtil": { + "label": "Channel Util" + }, + "airtimeUtil": { + "label": "Airtime Util" + } + }, + "nodesTable": { + "headings": { + "longName": "长名称", + "connection": "Connection", + "lastHeard": "Last Heard", + "encryption": "Encryption", + "model": "模型", + "macAddress": "MAC Address" + }, + "connectionStatus": { + "direct": "直频", + "away": "away", + "unknown": "-", + "viaMqtt": ", via MQTT" + }, + "lastHeardStatus": { + "never": "Never" + } + }, + "actions": { + "added": "Added", + "removed": "Removed", + "ignoreNode": "忽略节点", + "unignoreNode": "Unignore Node", + "requestPosition": "Request Position" + } +} diff --git a/packages/web/public/i18n/locales/zh-CN/ui.json b/packages/web/public/i18n/locales/zh-CN/ui.json new file mode 100644 index 00000000..a74f34f3 --- /dev/null +++ b/packages/web/public/i18n/locales/zh-CN/ui.json @@ -0,0 +1,228 @@ +{ + "navigation": { + "title": "Navigation", + "messages": "消息", + "map": "地图", + "config": "配置", + "radioConfig": "Radio Config", + "moduleConfig": "Module Config", + "channels": "频道", + "nodes": "节点" + }, + "app": { + "title": "Meshtastic", + "logo": "Meshtastic Logo" + }, + "sidebar": { + "collapseToggle": { + "button": { + "open": "Open sidebar", + "close": "Close sidebar" + } + }, + "deviceInfo": { + "volts": "{{voltage}} volts", + "firmware": { + "title": "固件", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" + } + }, + "batteryStatus": { + "charging": "{{level}}% charging", + "pluggedIn": "Plugged in", + "title": "电池" + }, + "search": { + "nodes": "Search nodes...", + "channels": "Search channels...", + "commandPalette": "Search commands..." + }, + "toast": { + "positionRequestSent": { + "title": "Position request sent." + }, + "requestingPosition": { + "title": "Requesting position, please wait..." + }, + "sendingTraceroute": { + "title": "Sending Traceroute, please wait..." + }, + "tracerouteSent": { + "title": "Traceroute sent." + }, + "savedChannel": { + "title": "Saved Channel: {{channelName}}" + }, + "messages": { + "pkiEncryption": { + "title": "Chat is using PKI encryption." + }, + "pskEncryption": { + "title": "Chat is using PSK encryption." + } + }, + "configSaveError": { + "title": "Error Saving Config", + "description": "An error occurred while saving the configuration." + }, + "validationError": { + "title": "Config Errors Exist", + "description": "Please fix the configuration errors before saving." + }, + "saveSuccess": { + "title": "Saving Config", + "description": "The configuration change {{case}} has been saved." + }, + "favoriteNode": { + "title": "{{action}} {{nodeName}} {{direction}} favorites.", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + }, + "ignoreNode": { + "title": "{{action}} {{nodeName}} {{direction}} ignore list", + "action": { + "added": "Added", + "removed": "Removed", + "to": "to", + "from": "from" + } + } + }, + "notifications": { + "copied": { + "label": "Copied!" + }, + "copyToClipboard": { + "label": "Copy to clipboard" + }, + "hidePassword": { + "label": "隐藏密码" + }, + "showPassword": { + "label": "显示密码" + }, + "deliveryStatus": { + "delivered": "Delivered", + "failed": "Delivery Failed", + "waiting": "等待中...", + "unknown": "未知" + } + }, + "general": { + "label": "General" + }, + "hardware": { + "label": "硬件" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "角色" + }, + "filter": { + "label": "筛选器csvfganw" + }, + "advanced": { + "label": "高级" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "电压" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "直频", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "最后听到", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "hopsUnknown": { + "label": "Unknown number of hops" + }, + "showUnheard": { + "label": "Never heard" + }, + "language": { + "label": "语言", + "changeLanguage": "Change Language" + }, + "theme": { + "dark": "深色", + "light": "浅色", + "system": "Automatic", + "changeTheme": "Change Color Scheme" + }, + "errorPage": { + "title": "This is a little embarrassing...", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", + "reportInstructions": "Please include the following information in your report:", + "reportSteps": { + "step1": "What you were doing when the error occurred", + "step2": "What you expected to happen", + "step3": "What actually happened", + "step4": "Any other relevant information" + }, + "reportLink": "You can report the issue to our <0>GitHub", + "dashboardLink": "Return to the <0>dashboard", + "detailsSummary": "Error Details", + "errorMessageLabel": "Error message:", + "stackTraceLabel": "Stack trace:", + "fallbackError": "{{error}}" + }, + "footer": { + "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", + "commitSha": "Commit SHA: {{sha}}" + } +} diff --git a/packages/web/public/icon.svg b/packages/web/public/icon.svg new file mode 100644 index 00000000..2d4a4fb6 --- /dev/null +++ b/packages/web/public/icon.svg @@ -0,0 +1,41 @@ + + + + Created with Fabric.js 4.6.0 + + + + + + + + + + + diff --git a/packages/web/public/logo.svg b/packages/web/public/logo.svg new file mode 100644 index 00000000..2d4a4fb6 --- /dev/null +++ b/packages/web/public/logo.svg @@ -0,0 +1,41 @@ + + + + Created with Fabric.js 4.6.0 + + + + + + + + + + + diff --git a/packages/web/public/logo_black.svg b/packages/web/public/logo_black.svg new file mode 100644 index 00000000..3568d300 --- /dev/null +++ b/packages/web/public/logo_black.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/packages/web/public/logo_white.svg b/packages/web/public/logo_white.svg new file mode 100644 index 00000000..7c5417ed --- /dev/null +++ b/packages/web/public/logo_white.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/public/robots.txt b/packages/web/public/robots.txt similarity index 100% rename from public/robots.txt rename to packages/web/public/robots.txt diff --git a/public/site.webmanifest b/packages/web/public/site.webmanifest similarity index 69% rename from public/site.webmanifest rename to packages/web/public/site.webmanifest index 6135ea5c..dc9509b9 100644 --- a/public/site.webmanifest +++ b/packages/web/public/site.webmanifest @@ -1,11 +1,11 @@ { "name": "Meshtastic", - "short_name": "Meshtastic", + "short_name": "Web Client", "start_url": ".", - "description": "Meshtastic web app", + "description": "Meshtastic Web App", "icons": [ { - "src": "/icon.svg", + "src": "/logo.svg", "sizes": "any", "type": "image/svg+xml" } diff --git a/src/App.tsx b/packages/web/src/App.tsx similarity index 100% rename from src/App.tsx rename to packages/web/src/App.tsx diff --git a/src/DeviceWrapper.tsx b/packages/web/src/DeviceWrapper.tsx similarity index 100% rename from src/DeviceWrapper.tsx rename to packages/web/src/DeviceWrapper.tsx diff --git a/src/__mocks__/README.md b/packages/web/src/__mocks__/README.md similarity index 100% rename from src/__mocks__/README.md rename to packages/web/src/__mocks__/README.md diff --git a/src/__mocks__/components/UI/Button.tsx b/packages/web/src/__mocks__/components/UI/Button.tsx similarity index 100% rename from src/__mocks__/components/UI/Button.tsx rename to packages/web/src/__mocks__/components/UI/Button.tsx diff --git a/src/__mocks__/components/UI/Checkbox.tsx b/packages/web/src/__mocks__/components/UI/Checkbox.tsx similarity index 100% rename from src/__mocks__/components/UI/Checkbox.tsx rename to packages/web/src/__mocks__/components/UI/Checkbox.tsx diff --git a/src/__mocks__/components/UI/Dialog/Dialog.tsx b/packages/web/src/__mocks__/components/UI/Dialog/Dialog.tsx similarity index 100% rename from src/__mocks__/components/UI/Dialog/Dialog.tsx rename to packages/web/src/__mocks__/components/UI/Dialog/Dialog.tsx diff --git a/src/__mocks__/components/UI/Label.tsx b/packages/web/src/__mocks__/components/UI/Label.tsx similarity index 100% rename from src/__mocks__/components/UI/Label.tsx rename to packages/web/src/__mocks__/components/UI/Label.tsx diff --git a/src/__mocks__/components/UI/Link.tsx b/packages/web/src/__mocks__/components/UI/Link.tsx similarity index 100% rename from src/__mocks__/components/UI/Link.tsx rename to packages/web/src/__mocks__/components/UI/Link.tsx diff --git a/packages/web/src/components/BatteryStatus.tsx b/packages/web/src/components/BatteryStatus.tsx new file mode 100644 index 00000000..feb7ff67 --- /dev/null +++ b/packages/web/src/components/BatteryStatus.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { + BatteryFullIcon, + BatteryLowIcon, + BatteryMediumIcon, + PlugZapIcon, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { DeviceMetrics } from "./types.ts"; + +type BatteryStatusKey = keyof typeof BATTERY_STATUS; + +interface BatteryStatusProps { + deviceMetrics?: DeviceMetrics | null; +} + +interface BatteryStatusProps { + deviceMetrics?: DeviceMetrics | null; +} + +interface StatusConfig { + Icon: React.ElementType; + className: string; + text: string; +} + +const BATTERY_STATUS = { + PLUGGED_IN: "PLUGGED_IN", + FULL: "FULL", + MEDIUM: "MEDIUM", + LOW: "LOW", +} as const; + +export const getBatteryStatus = (level: number): BatteryStatusKey => { + if (level > 100) { + return BATTERY_STATUS.PLUGGED_IN; + } + if (level > 80) { + return BATTERY_STATUS.FULL; + } + if (level > 20) { + return BATTERY_STATUS.MEDIUM; + } + return BATTERY_STATUS.LOW; +}; + +const BatteryStatus: React.FC = ({ deviceMetrics }) => { + const { t } = useTranslation(); + + if ( + deviceMetrics?.batteryLevel === undefined || + deviceMetrics?.batteryLevel === null + ) { + return null; + } + + const { batteryLevel } = deviceMetrics; + + const statusKey = getBatteryStatus(batteryLevel); + + const statusConfigMap: Record = { + [BATTERY_STATUS.PLUGGED_IN]: { + Icon: PlugZapIcon, + className: "text-gray-500", + text: t("batteryStatus.pluggedIn"), + }, + [BATTERY_STATUS.FULL]: { + Icon: BatteryFullIcon, + className: "text-green-500", + text: t("batteryStatus.charging", { level: batteryLevel }), + }, + [BATTERY_STATUS.MEDIUM]: { + Icon: BatteryMediumIcon, + className: "text-yellow-500", + text: t("batteryStatus.charging", { level: batteryLevel }), + }, + [BATTERY_STATUS.LOW]: { + Icon: BatteryLowIcon, + className: "text-red-500", + text: t("batteryStatus.charging", { level: batteryLevel }), + }, + }; + + // 3. Use the key to get the current state configuration + const { + Icon: BatteryIcon, + className: iconClassName, + text: statusText, + } = statusConfigMap[statusKey]; + + return ( +
+ + {statusText} +
+ ); +}; + +export default BatteryStatus; diff --git a/src/components/CommandPalette/index.tsx b/packages/web/src/components/CommandPalette/index.tsx similarity index 100% rename from src/components/CommandPalette/index.tsx rename to packages/web/src/components/CommandPalette/index.tsx diff --git a/src/components/ConnectionTabs/ConnectionTabs.tsx b/packages/web/src/components/ConnectionTabs/ConnectionTabs.tsx similarity index 100% rename from src/components/ConnectionTabs/ConnectionTabs.tsx rename to packages/web/src/components/ConnectionTabs/ConnectionTabs.tsx diff --git a/src/components/Dashboard/BluetoothSection.tsx b/packages/web/src/components/Dashboard/BluetoothSection.tsx similarity index 100% rename from src/components/Dashboard/BluetoothSection.tsx rename to packages/web/src/components/Dashboard/BluetoothSection.tsx diff --git a/src/components/Dashboard/HTTPServerSection.tsx b/packages/web/src/components/Dashboard/HTTPServerSection.tsx similarity index 100% rename from src/components/Dashboard/HTTPServerSection.tsx rename to packages/web/src/components/Dashboard/HTTPServerSection.tsx diff --git a/src/components/Dashboard/SerialSection.tsx b/packages/web/src/components/Dashboard/SerialSection.tsx similarity index 100% rename from src/components/Dashboard/SerialSection.tsx rename to packages/web/src/components/Dashboard/SerialSection.tsx diff --git a/src/components/DeviceInfoPanel.tsx b/packages/web/src/components/DeviceInfoPanel.tsx similarity index 100% rename from src/components/DeviceInfoPanel.tsx rename to packages/web/src/components/DeviceInfoPanel.tsx diff --git a/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.test.tsx b/packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.test.tsx similarity index 100% rename from src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.test.tsx rename to packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.test.tsx diff --git a/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx b/packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx similarity index 100% rename from src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx rename to packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx diff --git a/src/components/Dialog/DeviceNameDialog.tsx b/packages/web/src/components/Dialog/DeviceNameDialog.tsx similarity index 73% rename from src/components/Dialog/DeviceNameDialog.tsx rename to packages/web/src/components/Dialog/DeviceNameDialog.tsx index 6b4d5803..9944a2b3 100644 --- a/src/components/Dialog/DeviceNameDialog.tsx +++ b/packages/web/src/components/Dialog/DeviceNameDialog.tsx @@ -14,8 +14,9 @@ import { Protobuf } from "@meshtastic/core"; import { useForm } from "react-hook-form"; import { GenericInput } from "@components/Form/FormInput.tsx"; import { useTranslation } from "react-i18next"; -import { validateMaxByteLength } from "@core/utils/string.ts"; import { Label } from "../UI/Label.tsx"; +import z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; export interface User { longName: string; @@ -26,8 +27,6 @@ export interface DeviceNameDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } -const MAX_LONG_NAME_BYTE_LENGTH = 40; -const MAX_SHORT_NAME_BYTE_LENGTH = 4; export const DeviceNameDialog = ({ open, @@ -38,27 +37,29 @@ export const DeviceNameDialog = ({ const myNode = getNode(hardware.myNodeNum); const defaultValues = { - longName: myNode?.user?.longName ?? t("unknown.longName"), - shortName: myNode?.user?.shortName ?? t("unknown.shortName"), + shortName: myNode?.user?.shortName ?? "", + longName: myNode?.user?.longName ?? "", }; - const { getValues, setValue, reset, control, handleSubmit } = useForm({ - values: defaultValues, + const deviceNameSchema = z.object({ + longName: z + .string() + .min(1, t("deviceName.validation.longNameMin")) + .max(40, t("deviceName.validation.longNameMax")), + shortName: z + .string() + .min(2, t("deviceName.validation.shortNameMin")) + .max(4, t("deviceName.validation.shortNameMax")), }); - const { currentLength: currentLongNameLength } = validateMaxByteLength( - getValues("longName"), - MAX_LONG_NAME_BYTE_LENGTH, - ); - const { currentLength: currentShortNameLength } = validateMaxByteLength( - getValues("shortName"), - MAX_SHORT_NAME_BYTE_LENGTH, - ); + const { getValues, reset, control, handleSubmit } = useForm({ + values: defaultValues, + resolver: zodResolver(deviceNameSchema), + }); const onSubmit = handleSubmit((data) => { connection?.setOwner( create(Protobuf.Mesh.UserSchema, { - ...(myNode?.user ?? {}), ...data, }), ); @@ -66,9 +67,7 @@ export const DeviceNameDialog = ({ }); const handleReset = () => { - reset({ longName: "", shortName: "" }); - setValue("longName", ""); - setValue("shortName", ""); + reset(defaultValues); }; return ( @@ -95,8 +94,9 @@ export const DeviceNameDialog = ({ properties: { className: "text-slate-900 dark:text-slate-200", fieldLength: { - currentValueLength: currentLongNameLength ?? 0, - max: MAX_LONG_NAME_BYTE_LENGTH, + currentValueLength: getValues("longName").length, + max: 40, + min: 1, showCharacterCount: true, }, }, @@ -115,8 +115,9 @@ export const DeviceNameDialog = ({ type: "text", properties: { fieldLength: { - currentValueLength: currentShortNameLength ?? 0, - max: MAX_SHORT_NAME_BYTE_LENGTH, + currentValueLength: getValues("shortName").length, + max: 4, + min: 1, showCharacterCount: true, }, }, @@ -133,7 +134,12 @@ export const DeviceNameDialog = ({ > {t("button.reset")} - + diff --git a/src/components/Dialog/DialogManager.tsx b/packages/web/src/components/Dialog/DialogManager.tsx similarity index 100% rename from src/components/Dialog/DialogManager.tsx rename to packages/web/src/components/Dialog/DialogManager.tsx diff --git a/src/components/Dialog/ImportDialog.tsx b/packages/web/src/components/Dialog/ImportDialog.tsx similarity index 92% rename from src/components/Dialog/ImportDialog.tsx rename to packages/web/src/components/Dialog/ImportDialog.tsx index ac76d022..6b448ace 100644 --- a/src/components/Dialog/ImportDialog.tsx +++ b/packages/web/src/components/Dialog/ImportDialog.tsx @@ -72,17 +72,19 @@ export const ImportDialog = ({ }, [importDialogInput]); const apply = () => { - channelSet?.settings.map((ch: unknown, index: number) => { - connection?.setChannel( - create(Protobuf.Channel.ChannelSchema, { - index, - role: index === 0 - ? Protobuf.Channel.Channel_Role.PRIMARY - : Protobuf.Channel.Channel_Role.SECONDARY, - settings: ch, - }), - ); - }); + channelSet?.settings.map( + (ch: Protobuf.Channel.ChannelSettings, index: number) => { + connection?.setChannel( + create(Protobuf.Channel.ChannelSchema, { + index, + role: index === 0 + ? Protobuf.Channel.Channel_Role.PRIMARY + : Protobuf.Channel.Channel_Role.SECONDARY, + settings: ch, + }), + ); + }, + ); if (channelSet?.loraConfig) { connection?.setConfig( diff --git a/packages/web/src/components/Dialog/LocationResponseDialog.tsx b/packages/web/src/components/Dialog/LocationResponseDialog.tsx new file mode 100644 index 00000000..eb6d5e25 --- /dev/null +++ b/packages/web/src/components/Dialog/LocationResponseDialog.tsx @@ -0,0 +1,93 @@ +import { useDevice } from "@core/stores/deviceStore.ts"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "../UI/Dialog.tsx"; +import type { Protobuf, Types } from "@meshtastic/core"; +import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; +import { useTranslation } from "react-i18next"; + +export interface LocationResponseDialogProps { + location: Types.PacketMetadata | undefined; + open: boolean; + onOpenChange: () => void; +} + +export const LocationResponseDialog = ({ + location, + open, + onOpenChange, +}: LocationResponseDialogProps) => { + const { t } = useTranslation("dialog"); + const { getNode } = useDevice(); + + const from = getNode(location?.from ?? 0); + const longName = from?.user?.longName ?? + (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName")); + const shortName = from?.user?.shortName ?? + (from + ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` + : t("unknown.shortName")); + + const position = location?.data; + + const hasCoordinates = position && + typeof position.latitudeI === "number" && + typeof position.longitudeI === "number" && + typeof position.altitude === "number"; + + return ( + + + + + + {t("locationResponse.title", { + identifier: `${longName} (${shortName})`, + })} + + + + {hasCoordinates + ? ( +
+ +

+ {t("locationResponse.coordinates")} + + {" "} + {position.latitudeI ?? 0 / 1e7},{" "} + {position.longitudeI ?? 0 / 1e7} + +

+

+ {t("locationResponse.altitude")} {position.altitude} + {(position.altitude ?? 0) < 1 + ? t("unit.meter.one") + : t("unit.meter.plural")} +

+
+
+ ) + : ( + // Optional: Show a message if coordinates are not available +

+ {t("locationResponse.noCoordinates")} +

+ )} +
+
+
+ ); +}; diff --git a/packages/web/src/components/Dialog/ManagedModeDialog.tsx b/packages/web/src/components/Dialog/ManagedModeDialog.tsx new file mode 100644 index 00000000..996139d4 --- /dev/null +++ b/packages/web/src/components/Dialog/ManagedModeDialog.tsx @@ -0,0 +1,72 @@ +import { Button } from "@components/UI/Button.tsx"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog.tsx"; +import { Trans, useTranslation } from "react-i18next"; +import { Checkbox } from "@components/UI/Checkbox/index.tsx"; +import { useState } from "react"; + +export interface ManagedModeDialogProps { + open: boolean; + onOpenChange: () => void; + onSubmit: () => void; +} + +export const ManagedModeDialog = ({ + open, + onOpenChange, + onSubmit, +}: ManagedModeDialogProps) => { + const { t } = useTranslation("dialog"); + const [confirmState, setConfirmState] = useState(false); + + return ( + + + + + {t("managedMode.title")} + + , + }} + /> + + +
+ setConfirmState(!confirmState)} + name="confirmUnderstanding" + > +

+ {t("managedMode.confirmUnderstanding")} +

+
+
+ + + +
+
+ ); +}; diff --git a/src/components/Dialog/NewConnectionDialog.tsx b/packages/web/src/components/Dialog/NewConnectionDialog.tsx similarity index 100% rename from src/components/Dialog/NewConnectionDialog.tsx rename to packages/web/src/components/Dialog/NewConnectionDialog.tsx diff --git a/src/components/Dialog/NewDeviceDialog.tsx b/packages/web/src/components/Dialog/NewDeviceDialog.tsx similarity index 60% rename from src/components/Dialog/NewDeviceDialog.tsx rename to packages/web/src/components/Dialog/NewDeviceDialog.tsx index f821c05b..eb25e185 100644 --- a/src/components/Dialog/NewDeviceDialog.tsx +++ b/packages/web/src/components/Dialog/NewDeviceDialog.tsx @@ -18,9 +18,7 @@ import { TabsList, TabsTrigger, } from "@components/UI/Tabs.tsx"; -import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { AlertCircle } from "lucide-react"; -import { useMemo } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Link } from "../UI/Typography/Link.tsx"; @@ -29,6 +27,7 @@ export interface TabElementProps { } export interface TabManifest { + id: "HTTP" | "BLE" | "Serial"; label: string; element: React.FC; isDisabled: boolean; @@ -41,29 +40,28 @@ export interface NewDeviceProps { interface FeatureErrorProps { missingFeatures: BrowserFeature[]; + tabId: "HTTP" | "BLE" | "Serial"; } -const links: { [key: string]: string } = { - "Web Bluetooth": - "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", - "Web Serial": - "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", - "Secure Context": - "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", +const errors: Record = { + "Web Bluetooth": { + href: + "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", + i18nKey: "newDeviceDialog.validation.requiresWebBluetooth", + }, + "Web Serial": { + href: + "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", + i18nKey: "newDeviceDialog.validation.requiresWebSerial", + }, + "Secure Context": { + href: + "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", + i18nKey: "newDeviceDialog.validation.requiresSecureContext", + }, }; -const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { - const { i18n } = useTranslation("dialog"); - - const listFormatter = useMemo( - () => - new Intl.ListFormat(i18n.language, { - style: "long", - type: "disjunction", - }), - [i18n.language], - ); - +const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => { if (missingFeatures.length === 0) return null; const browserFeatures = missingFeatures.filter( @@ -71,37 +69,32 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { ); const needsSecureContext = missingFeatures.includes("Secure Context"); - const formatFeatureList = (features: string[]) => { - const parts = listFormatter.formatToParts(features); - return parts.map((part) => { - if (part.type === "element") { - return ( - - {part.value} - - ); - } - return {part.value}; - }); - }; - - const featureNodes = formatFeatureList(browserFeatures); + const needsFeature = + (tabId === "BLE" && browserFeatures.includes("Web Bluetooth")) + ? "Web Bluetooth" + : (tabId === "Serial" && browserFeatures.includes("Web Serial")) + ? "Web Serial" + : undefined; return ( - +
-

- {browserFeatures.length > 0 && ( +

+ {needsFeature && ( {featureNodes}, - }} + i18nKey={errors[needsFeature].i18nKey} + components={[ + , + ]} /> )} - {browserFeatures.length > 0 && needsSecureContext && " "} + {needsFeature && needsSecureContext && " "} {needsSecureContext && ( 0 @@ -110,17 +103,17 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { components={{ "0": ( ), }} /> )} -

+
- +
); }; @@ -133,17 +126,20 @@ export const NewDeviceDialog = ({ const tabs: TabManifest[] = [ { + id: "HTTP", label: t("newDeviceDialog.tabHttp"), element: HTTP, isDisabled: false, }, { + id: "BLE", label: t("newDeviceDialog.tabBluetooth"), element: BLE, isDisabled: unsupported.includes("Web Bluetooth") || unsupported.includes("Secure Context"), }, { + id: "Serial", label: t("newDeviceDialog.tabSerial"), element: Serial, isDisabled: unsupported.includes("Web Serial") || @@ -161,21 +157,27 @@ export const NewDeviceDialog = ({ {tabs.map((tab) => ( - + {tab.label} ))} {tabs.map((tab) => ( - +
- {(tab.label !== "HTTP" && + {(tab.id !== "HTTP" && tab.isDisabled) - ? - : null} - onOpenChange(false)} - /> + ? ( + + ) + : ( + onOpenChange(false)} + /> + )}
))} diff --git a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx b/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx similarity index 99% rename from src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx rename to packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx index 9a80194d..356ab3b1 100644 --- a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx +++ b/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx @@ -82,6 +82,7 @@ export const NodeDetailsDialog = ({ function handleDirectMessage() { if (!node) return; navigate({ to: `/messages/direct/${node.num}` }); + setDialogOpen("nodeDetails", false); } function handleRequestPosition() { diff --git a/src/components/Dialog/PKIBackupDialog.tsx b/packages/web/src/components/Dialog/PKIBackupDialog.tsx similarity index 100% rename from src/components/Dialog/PKIBackupDialog.tsx rename to packages/web/src/components/Dialog/PKIBackupDialog.tsx diff --git a/src/components/Dialog/PkiRegenerateDialog.tsx b/packages/web/src/components/Dialog/PkiRegenerateDialog.tsx similarity index 100% rename from src/components/Dialog/PkiRegenerateDialog.tsx rename to packages/web/src/components/Dialog/PkiRegenerateDialog.tsx diff --git a/src/components/Dialog/QRDialog.tsx b/packages/web/src/components/Dialog/QRDialog.tsx similarity index 94% rename from src/components/Dialog/QRDialog.tsx rename to packages/web/src/components/Dialog/QRDialog.tsx index ae3d259f..0cb914e0 100644 --- a/src/components/Dialog/QRDialog.tsx +++ b/packages/web/src/components/Dialog/QRDialog.tsx @@ -13,7 +13,6 @@ import { Input } from "@components/UI/Input.tsx"; import { Label } from "@components/UI/Label.tsx"; import { Protobuf, type Types } from "@meshtastic/core"; import { fromByteArray } from "base64-js"; -import { ClipboardIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { QRCode } from "react-qrcode-logo"; import { useTranslation } from "react-i18next"; @@ -92,7 +91,7 @@ export const QRDialog = ({ { + onChange={() => { if (selectedChannels.includes(channel.index)) { setSelectedChannels( selectedChannels.filter((c) => @@ -144,13 +143,6 @@ export const QRDialog = ({ diff --git a/src/components/Dialog/RebootDialog.tsx b/packages/web/src/components/Dialog/RebootDialog.tsx similarity index 86% rename from src/components/Dialog/RebootDialog.tsx rename to packages/web/src/components/Dialog/RebootDialog.tsx index 9b59baa2..7ed48ce6 100644 --- a/src/components/Dialog/RebootDialog.tsx +++ b/packages/web/src/components/Dialog/RebootDialog.tsx @@ -9,7 +9,7 @@ import { } from "@components/UI/Dialog.tsx"; import { Input } from "@components/UI/Input.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; -import { ClockIcon, RefreshCwIcon } from "lucide-react"; +import { RefreshCwIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useState } from "react"; @@ -45,12 +45,6 @@ export const RebootDialog = ({ className="dark:text-slate-900" value={time} onChange={(e) => setTime(Number.parseInt(e.target.value))} - action={{ - icon: ClockIcon, - onClick() { - connection?.reboot(time * 60).then(() => onOpenChange(false)); - }, - }} /> )} diff --git a/src/components/Form/DynamicFormField.tsx b/packages/web/src/components/Form/DynamicFormField.tsx similarity index 73% rename from src/components/Form/DynamicFormField.tsx rename to packages/web/src/components/Form/DynamicFormField.tsx index 320db39f..34508613 100644 --- a/src/components/Form/DynamicFormField.tsx +++ b/packages/web/src/components/Form/DynamicFormField.tsx @@ -31,19 +31,29 @@ export interface DynamicFormFieldProps { field: FieldProps; control: Control; disabled?: boolean; + isDirty?: boolean; + invalid?: boolean; } export function DynamicFormField({ field, control, disabled, + isDirty, + invalid, }: DynamicFormFieldProps) { switch (field.type) { case "text": case "password": case "number": return ( - + ); case "toggle": @@ -52,6 +62,8 @@ export function DynamicFormField({ field={field} control={control} disabled={disabled} + isDirty={isDirty} + invalid={invalid} /> ); case "select": @@ -60,6 +72,8 @@ export function DynamicFormField({ field={field} control={control} disabled={disabled} + isDirty={isDirty} + invalid={invalid} /> ); case "passwordGenerator": @@ -68,11 +82,19 @@ export function DynamicFormField({ field={field} control={control} disabled={disabled} + isDirty={isDirty} + invalid={invalid} /> ); case "multiSelect": return ( - + ); } } diff --git a/src/components/Form/FormInput.tsx b/packages/web/src/components/Form/FormInput.tsx similarity index 70% rename from src/components/Form/FormInput.tsx rename to packages/web/src/components/Form/FormInput.tsx index c0af74ae..9a7d605d 100644 --- a/src/components/Form/FormInput.tsx +++ b/packages/web/src/components/Form/FormInput.tsx @@ -4,15 +4,14 @@ import type { } from "@components/Form/DynamicForm.tsx"; import { Input } from "@components/UI/Input.tsx"; import type { ChangeEventHandler } from "react"; -import { useState } from "react"; import { type FieldValues, useController } from "react-hook-form"; export interface InputFieldProps extends BaseFormBuilderProps { type: "text" | "number" | "password"; inputChange?: ChangeEventHandler; + prefix?: string; properties?: { - value?: string; - prefix?: string; + id?: string; suffix?: string; step?: number; className?: string; @@ -31,27 +30,33 @@ export function GenericInput({ control, disabled, field, + invalid, }: GenericFormElementProps>) { const { fieldLength, ...restProperties } = field.properties || {}; - const [currentLength, setCurrentLength] = useState( - fieldLength?.currentValueLength || 0, - ); - const { field: controllerField } = useController({ + const { + field: controllerField, + fieldState: { error, isDirty }, + } = useController({ name: field.name, control, + rules: { + minLength: field.properties?.fieldLength?.min, + maxLength: field.properties?.fieldLength?.max, + }, }); + const isInvalid = invalid || Boolean(error?.message); + const handleInputChange = (e: React.ChangeEvent) => { const newValue = e.target.value; if ( field.properties?.fieldLength?.max && - newValue.length > field.properties?.fieldLength?.max + newValue.length > field.properties.fieldLength.max ) { return; } - setCurrentLength(newValue.length); if (field.inputChange) field.inputChange(e); @@ -62,6 +67,10 @@ export function GenericInput({ ); }; + const currentLength = controllerField.value + ? String(controllerField.value).length + : 0; + return (
({ className={field.properties?.className} {...restProperties} disabled={disabled} + variant={error ? "invalid" : isDirty ? "dirty" : "default"} /> - {fieldLength?.showCharacterCount && fieldLength?.max && ( + {fieldLength?.showCharacterCount && fieldLength.max && (
- {currentLength ?? fieldLength?.currentValueLength}/{fieldLength?.max} + {currentLength}/{fieldLength.max} +
+ )} + + {isInvalid && ( +
+

{error?.message ?? ""}

)}
diff --git a/src/components/Form/FormMultiSelect.tsx b/packages/web/src/components/Form/FormMultiSelect.tsx similarity index 82% rename from src/components/Form/FormMultiSelect.tsx rename to packages/web/src/components/Form/FormMultiSelect.tsx index cea9a511..82354c88 100644 --- a/src/components/Form/FormMultiSelect.tsx +++ b/packages/web/src/components/Form/FormMultiSelect.tsx @@ -6,6 +6,7 @@ import type { FieldValues } from "react-hook-form"; import { useTranslation } from "react-i18next"; import type { FLAGS_CONFIG } from "@core/hooks/usePositionFlags.ts"; import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect.tsx"; +import { cn } from "@core/utils/cn.ts"; export interface MultiSelectFieldProps extends BaseFormBuilderProps { type: "multiSelect"; @@ -23,9 +24,11 @@ export interface MultiSelectFieldProps extends BaseFormBuilderProps { export function MultiSelectInput({ field, + isDirty, + invalid, }: GenericFormElementProps>) { const { t } = useTranslation("deviceConfig"); - const { enumValue, ...remainingProperties } = field.properties; + const { enumValue, className, ...remainingProperties } = field.properties; const isNewConfigStructure = typeof Object.values(enumValue)[0] === "object" && @@ -48,7 +51,15 @@ export function MultiSelectInput({ ); return ( - + {optionsToRender.map((option) => { return ( ({ control, field, disabled, + isDirty, + invalid, }: GenericFormElementProps>) { const { isVisible } = usePasswordVisibilityToggle(); const { trigger } = useFormContext(); @@ -42,7 +44,11 @@ export function PasswordGenerator({ ( + render={( + { + field: { value, onChange, ...rest }, + }, + ) => ( ({ }} selectChange={field.selectChange ?? (() => {})} value={value} - variant={field.validationText ? "invalid" : "default"} + variant={invalid ? "invalid" : isDirty ? "dirty" : "default"} actionButtons={field.actionButtons} showPasswordToggle={field.showPasswordToggle} showCopyButton={field.showCopyButton} diff --git a/src/components/Form/FormSelect.tsx b/packages/web/src/components/Form/FormSelect.tsx similarity index 77% rename from src/components/Form/FormSelect.tsx rename to packages/web/src/components/Form/FormSelect.tsx index 5b33ea1d..3b4cce04 100644 --- a/src/components/Form/FormSelect.tsx +++ b/packages/web/src/components/Form/FormSelect.tsx @@ -10,6 +10,7 @@ import { SelectValue, } from "@components/UI/Select.tsx"; import { type FieldValues, useController } from "react-hook-form"; +import { cn } from "@core/utils/cn.ts"; export interface SelectFieldProps extends BaseFormBuilderProps { type: "select"; @@ -38,16 +39,23 @@ export function SelectInput({ control, disabled, field, + isDirty, + invalid, }: GenericFormElementProps>) { const { - field: { value, onChange, ...rest }, + field: { value, onChange, ref, onBlur, ...rest }, } = useController({ name: field.name, control, }); - const { enumValue, formatEnumName, ...remainingProperties } = - field.properties; + const { + enumValue, + formatEnumName, + defaultValue, + className, + ...remainingProperties + } = field.properties; const valueToKeyMap: Record = {}; const optionsEnumValues: [string, number][] = []; @@ -77,10 +85,22 @@ export function SelectInput({ onValueChange={handleValueChange} disabled={disabled} value={value?.toString()} - {...remainingProperties} + defaultValue={defaultValue?.toString()} {...rest} > - + diff --git a/src/components/Form/FormToggle.tsx b/packages/web/src/components/Form/FormToggle.tsx similarity index 66% rename from src/components/Form/FormToggle.tsx rename to packages/web/src/components/Form/FormToggle.tsx index 4b006c2f..6351ef51 100644 --- a/src/components/Form/FormToggle.tsx +++ b/packages/web/src/components/Form/FormToggle.tsx @@ -4,6 +4,7 @@ import type { } from "@components/Form/DynamicForm.tsx"; import { Switch } from "@components/UI/Switch.tsx"; import { Controller, type FieldValues } from "react-hook-form"; +import { cn } from "@core/utils/cn.ts"; export interface ToggleFieldProps extends BaseFormBuilderProps { type: "toggle"; @@ -14,12 +15,16 @@ export function ToggleInput({ control, disabled, field, + isDirty, + invalid, }: GenericFormElementProps>) { return ( ( + render={( + { field: { value, onChange, ...rest } }, + ) => ( { @@ -29,6 +34,15 @@ export function ToggleInput({ id={field.name} disabled={disabled} {...field.properties} + className={cn([ + field.properties?.className, + isDirty + ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" + : "", + invalid + ? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2" + : "", + ])} {...rest} /> )} diff --git a/src/components/Form/FormWrapper.tsx b/packages/web/src/components/Form/FormWrapper.tsx similarity index 100% rename from src/components/Form/FormWrapper.tsx rename to packages/web/src/components/Form/FormWrapper.tsx diff --git a/src/components/Form/createZodResolver.ts b/packages/web/src/components/Form/createZodResolver.ts similarity index 100% rename from src/components/Form/createZodResolver.ts rename to packages/web/src/components/Form/createZodResolver.ts diff --git a/src/components/KeyBackupReminder.tsx b/packages/web/src/components/KeyBackupReminder.tsx similarity index 90% rename from src/components/KeyBackupReminder.tsx rename to packages/web/src/components/KeyBackupReminder.tsx index aefe60ec..32cacd1a 100644 --- a/src/components/KeyBackupReminder.tsx +++ b/packages/web/src/components/KeyBackupReminder.tsx @@ -7,7 +7,7 @@ export const KeyBackupReminder = () => { const { t } = useTranslation("dialog"); useBackupReminder({ - message: t("pkiBackup.description"), + message: t("pkiBackupReminder.description"), onAccept: () => setDialogOpen("pkiBackup", true), enabled: true, }); diff --git a/src/components/LanguageSwitcher.tsx b/packages/web/src/components/LanguageSwitcher.tsx similarity index 83% rename from src/components/LanguageSwitcher.tsx rename to packages/web/src/components/LanguageSwitcher.tsx index 03db9d9c..62a2b4cf 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/packages/web/src/components/LanguageSwitcher.tsx @@ -1,6 +1,6 @@ import { Check, Languages } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { LangCode, supportedLanguages } from "../i18n/config.ts"; +import { LangCode, supportedLanguages } from "@app/i18n-config.ts"; import useLang from "@core/hooks/useLang.ts"; import { DropdownMenu, @@ -16,15 +16,11 @@ interface LanguageSwitcherProps { disableHover?: boolean; } -export default function LanguageSwitcher( - { disableHover = false }: LanguageSwitcherProps, -) { +export default function LanguageSwitcher({ + disableHover = false, +}: LanguageSwitcherProps) { const { i18n } = useTranslation("ui"); - const { set: setLanguage } = useLang(); - - const currentLanguage = - supportedLanguages.find((lang) => lang.code === i18n.language) || - supportedLanguages[0]; + const { set: setLanguage, currentLanguage } = useLang(); const handleLanguageChange = async (languageCode: LangCode) => { await setLanguage(languageCode, true); @@ -65,15 +61,15 @@ export default function LanguageSwitcher( "group-hover:text-gray-900 dark:group-hover:text-white", )} > - {currentLanguage.code.toUpperCase()} + {currentLanguage?.name}
- + {supportedLanguages.map((language) => ( handleLanguageChange(language.code as LangCode)} + onClick={() => handleLanguageChange(language.code)} className="flex items-center justify-between cursor-pointer" >
diff --git a/src/components/Map.tsx b/packages/web/src/components/Map.tsx similarity index 89% rename from src/components/Map.tsx rename to packages/web/src/components/Map.tsx index 9a3946f5..a121fb6e 100644 --- a/src/components/Map.tsx +++ b/packages/web/src/components/Map.tsx @@ -1,6 +1,5 @@ import MapGl, { AttributionControl, - GeolocateControl, type MapRef, NavigationControl, ScaleControl, @@ -45,11 +44,15 @@ export const Map = ({ children, onLoad }: MapProps) => { color: darkMode ? "black" : undefined, }} /> - + /> */ + } {children} diff --git a/src/components/PageComponents/Channel.tsx b/packages/web/src/components/PageComponents/Channel.tsx similarity index 100% rename from src/components/PageComponents/Channel.tsx rename to packages/web/src/components/PageComponents/Channel.tsx diff --git a/src/components/PageComponents/Config/Bluetooth.tsx b/packages/web/src/components/PageComponents/Config/Bluetooth.tsx similarity index 75% rename from src/components/PageComponents/Config/Bluetooth.tsx rename to packages/web/src/components/PageComponents/Config/Bluetooth.tsx index efd1111c..5d27c4a1 100644 --- a/src/components/PageComponents/Config/Bluetooth.tsx +++ b/packages/web/src/components/PageComponents/Config/Bluetooth.tsx @@ -3,16 +3,29 @@ import { BluetoothValidationSchema, } from "@app/validation/config/bluetooth.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -export const Bluetooth = () => { - const { config, setWorkingConfig } = useDevice(); +interface BluetoothConfigProps { + onFormInit: DynamicFormFormInit; +} +export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => { + const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } = + useDevice(); const { t } = useTranslation("deviceConfig"); const onSubmit = (data: BluetoothValidation) => { + if (deepCompareConfig(config.bluetooth, data, true)) { + removeWorkingConfig("bluetooth"); + return; + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -26,9 +39,11 @@ export const Bluetooth = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={BluetoothValidationSchema} formId="Config_BluetoothConfig" defaultValues={config.bluetooth} + values={getEffectiveConfig("bluetooth")} fieldGroups={[ { label: t("bluetooth.title"), diff --git a/packages/web/src/components/PageComponents/Config/ConfigSuspender.tsx b/packages/web/src/components/PageComponents/Config/ConfigSuspender.tsx new file mode 100644 index 00000000..83eaa89d --- /dev/null +++ b/packages/web/src/components/PageComponents/Config/ConfigSuspender.tsx @@ -0,0 +1,37 @@ +import { + useDevice, + ValidConfigType, + ValidModuleConfigType, +} from "@core/stores/deviceStore.ts"; +import { useEffect, useState } from "react"; + +export function ConfigSuspender({ + configCase, + moduleConfigCase, + children, +}: { + configCase?: ValidConfigType; + moduleConfigCase?: ValidModuleConfigType; + children: React.ReactNode; +}) { + const { config, moduleConfig } = useDevice(); + + let cfg = undefined; + if (configCase) { + cfg = config[configCase]; + } else if (moduleConfigCase) { + cfg = moduleConfig[moduleConfigCase]; + } else { + return children; + } + + const [ready, setReady] = useState(() => cfg !== undefined); + + useEffect(() => { + if (cfg !== undefined) setReady(true); + }, [cfg]); + + if (!ready) throw new Promise(() => {}); // triggers suspense fallback + + return children; +} diff --git a/src/components/PageComponents/Config/Device/index.tsx b/packages/web/src/components/PageComponents/Config/Device/index.tsx similarity index 84% rename from src/components/PageComponents/Config/Device/index.tsx rename to packages/web/src/components/PageComponents/Config/Device/index.tsx index 68e4b8ad..2d1689b2 100644 --- a/src/components/PageComponents/Config/Device/index.tsx +++ b/packages/web/src/components/PageComponents/Config/Device/index.tsx @@ -3,18 +3,31 @@ import { DeviceValidationSchema, } from "@app/validation/config/device.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -export const Device = () => { - const { config, setWorkingConfig } = useDevice(); +interface DeviceConfigProps { + onFormInit: DynamicFormFormInit; +} +export const Device = ({ onFormInit }: DeviceConfigProps) => { + const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } = + useDevice(); const { t } = useTranslation("deviceConfig"); const { validateRoleSelection } = useUnsafeRolesDialog(); const onSubmit = (data: DeviceValidation) => { + if (deepCompareConfig(config.device, data, true)) { + removeWorkingConfig("device"); + return; + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -24,12 +37,15 @@ export const Device = () => { }), ); }; + return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={DeviceValidationSchema} formId="Config_DeviceConfig" defaultValues={config.device} + values={getEffectiveConfig("device")} fieldGroups={[ { label: t("device.title"), @@ -97,7 +113,8 @@ export const Device = () => { properties: { fieldLength: { max: 64, - currentValueLength: config.device?.tzdef?.length, + currentValueLength: getEffectiveConfig("device")?.tzdef + ?.length, showCharacterCount: true, }, }, diff --git a/src/components/PageComponents/Config/Display.tsx b/packages/web/src/components/PageComponents/Config/Display.tsx similarity index 87% rename from src/components/PageComponents/Config/Display.tsx rename to packages/web/src/components/PageComponents/Config/Display.tsx index 0cda0f33..42e0e546 100644 --- a/src/components/PageComponents/Config/Display.tsx +++ b/packages/web/src/components/PageComponents/Config/Display.tsx @@ -3,16 +3,29 @@ import { DisplayValidationSchema, } from "@app/validation/config/display.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -export const Display = () => { - const { config, setWorkingConfig } = useDevice(); +interface DisplayConfigProps { + onFormInit: DynamicFormFormInit; +} +export const Display = ({ onFormInit }: DisplayConfigProps) => { + const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } = + useDevice(); const { t } = useTranslation("deviceConfig"); const onSubmit = (data: DisplayValidation) => { + if (deepCompareConfig(config.display, data, true)) { + removeWorkingConfig("display"); + return; + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -26,9 +39,11 @@ export const Display = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={DisplayValidationSchema} formId="Config_DisplayConfig" defaultValues={config.display} + values={getEffectiveConfig("display")} fieldGroups={[ { label: t("display.title"), diff --git a/src/components/PageComponents/Config/LoRa.tsx b/packages/web/src/components/PageComponents/Config/LoRa.tsx similarity index 91% rename from src/components/PageComponents/Config/LoRa.tsx rename to packages/web/src/components/PageComponents/Config/LoRa.tsx index cf3abdd5..251f2fcb 100644 --- a/src/components/PageComponents/Config/LoRa.tsx +++ b/packages/web/src/components/PageComponents/Config/LoRa.tsx @@ -3,16 +3,29 @@ import { LoRaValidationSchema, } from "@app/validation/config/lora.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -export const LoRa = () => { - const { config, setWorkingConfig } = useDevice(); +interface LoRaConfigProps { + onFormInit: DynamicFormFormInit; +} +export const LoRa = ({ onFormInit }: LoRaConfigProps) => { + const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } = + useDevice(); const { t } = useTranslation("deviceConfig"); const onSubmit = (data: LoRaValidation) => { + if (deepCompareConfig(config.lora, data, true)) { + removeWorkingConfig("lora"); + return; + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -26,9 +39,11 @@ export const LoRa = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={LoRaValidationSchema} formId="Config_LoRaConfig" defaultValues={config.lora} + values={getEffectiveConfig("lora")} fieldGroups={[ { label: t("lora.title"), diff --git a/src/components/PageComponents/Config/Network/index.tsx b/packages/web/src/components/PageComponents/Config/Network/index.tsx similarity index 78% rename from src/components/PageComponents/Config/Network/index.tsx rename to packages/web/src/components/PageComponents/Config/Network/index.tsx index 3d944668..12e72362 100644 --- a/src/components/PageComponents/Config/Network/index.tsx +++ b/packages/web/src/components/PageComponents/Config/Network/index.tsx @@ -3,7 +3,10 @@ import { NetworkValidationSchema, } from "@app/validation/config/network.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { convertIntToIpAddress, @@ -11,35 +14,50 @@ import { } from "@core/utils/ip.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -export const Network = () => { - const { config, setWorkingConfig } = useDevice(); +interface NetworkConfigProps { + onFormInit: DynamicFormFormInit; +} +export const Network = ({ onFormInit }: NetworkConfigProps) => { + const { config, setWorkingConfig, getEffectiveConfig, removeWorkingConfig } = + useDevice(); const { t } = useTranslation("deviceConfig"); + + const networkConfig = getEffectiveConfig("network"); + const onSubmit = (data: NetworkValidation) => { + const payload = { + ...data, + ipv4Config: create( + Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, + { + ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""), + gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""), + subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""), + dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""), + }, + ), + }; + + if (deepCompareConfig(config.network, payload, true)) { + removeWorkingConfig("network"); + return; + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { case: "network", - value: { - ...data, - ipv4Config: create( - Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, - { - ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""), - gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""), - subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""), - dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""), - }, - ), - }, + value: payload, }, }), ); }; - return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={NetworkValidationSchema} formId="Config_NetworkConfig" defaultValues={{ @@ -57,6 +75,21 @@ export const Network = () => { enabledProtocols: config.network?.enabledProtocols ?? Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST, }} + values={{ + ...networkConfig, + ipv4Config: { + ip: convertIntToIpAddress(networkConfig?.ipv4Config?.ip ?? 0), + gateway: convertIntToIpAddress( + networkConfig?.ipv4Config?.gateway ?? 0, + ), + subnet: convertIntToIpAddress( + networkConfig?.ipv4Config?.subnet ?? 0, + ), + dns: convertIntToIpAddress(networkConfig?.ipv4Config?.dns ?? 0), + }, + enabledProtocols: networkConfig?.enabledProtocols ?? + Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST, + } as NetworkValidation} fieldGroups={[ { label: t("network.title"), diff --git a/src/components/PageComponents/Config/Position.tsx b/packages/web/src/components/PageComponents/Config/Position.tsx similarity index 88% rename from src/components/PageComponents/Config/Position.tsx rename to packages/web/src/components/PageComponents/Config/Position.tsx index b306117c..d6fa1a05 100644 --- a/src/components/PageComponents/Config/Position.tsx +++ b/packages/web/src/components/PageComponents/Config/Position.tsx @@ -7,20 +7,33 @@ import { PositionValidationSchema, } from "@app/validation/config/position.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -export const Position = () => { - const { config, setWorkingConfig } = useDevice(); +interface PositionConfigProps { + onFormInit: DynamicFormFormInit; +} +export const Position = ({ onFormInit }: PositionConfigProps) => { + const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } = + useDevice(); const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags( - config?.position?.positionFlags ?? 0, + getEffectiveConfig("position")?.positionFlags ?? 0, ); const { t } = useTranslation("deviceConfig"); const onSubmit = (data: PositionValidation) => { + if (deepCompareConfig(config.position, data, true)) { + removeWorkingConfig("position"); + return; + } + return setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -44,9 +57,11 @@ export const Position = () => { data.positionFlags = flagsValue; return onSubmit(data); }} + onFormInit={onFormInit} validationSchema={PositionValidationSchema} formId="Config_PositionConfig" defaultValues={config.position} + values={getEffectiveConfig("position")} fieldGroups={[ { label: t("position.title"), diff --git a/src/components/PageComponents/Config/Power.tsx b/packages/web/src/components/PageComponents/Config/Power.tsx similarity index 85% rename from src/components/PageComponents/Config/Power.tsx rename to packages/web/src/components/PageComponents/Config/Power.tsx index b81e1aaa..c35c543d 100644 --- a/src/components/PageComponents/Config/Power.tsx +++ b/packages/web/src/components/PageComponents/Config/Power.tsx @@ -3,16 +3,29 @@ import { PowerValidationSchema, } from "@app/validation/config/power.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -export const Power = () => { - const { config, setWorkingConfig } = useDevice(); +interface PowerConfigProps { + onFormInit: DynamicFormFormInit; +} +export const Power = ({ onFormInit }: PowerConfigProps) => { + const { setWorkingConfig, config, getEffectiveConfig, removeWorkingConfig } = + useDevice(); const { t } = useTranslation("deviceConfig"); const onSubmit = (data: PowerValidation) => { + if (deepCompareConfig(config.power, data, true)) { + removeWorkingConfig("power"); + return; + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -26,9 +39,11 @@ export const Power = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={PowerValidationSchema} formId="Config_PowerConfig" defaultValues={config.power} + values={getEffectiveConfig("power")} fieldGroups={[ { label: t("power.powerConfigSettings.label"), diff --git a/src/components/PageComponents/Config/Security/Security.tsx b/packages/web/src/components/PageComponents/Config/Security/Security.tsx similarity index 69% rename from src/components/PageComponents/Config/Security/Security.tsx rename to packages/web/src/components/PageComponents/Config/Security/Security.tsx index 5b3f6e77..911227b5 100644 --- a/src/components/PageComponents/Config/Security/Security.tsx +++ b/packages/web/src/components/PageComponents/Config/Security/Security.tsx @@ -1,5 +1,9 @@ import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useAppStore } from "@core/stores/appStore.ts"; import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts"; import { @@ -7,37 +11,82 @@ import { type RawSecurity, RawSecuritySchema, } from "@app/validation/config/security.ts"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { create } from "@bufbuild/protobuf"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { fromByteArray, toByteArray } from "base64-js"; import { useTranslation } from "react-i18next"; +import { type DefaultValues, useForm } from "react-hook-form"; +import { createZodResolver } from "@components/Form/createZodResolver.ts"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -type KeyState = { - publicKey: string; - privateKey: string; - privateKeyDialogOpen: boolean; -}; +interface SecurityConfigProps { + onFormInit: DynamicFormFormInit; +} +export const Security = ({ onFormInit }: SecurityConfigProps) => { + const { + config, + setWorkingConfig, + setDialogOpen, + getEffectiveConfig, + removeWorkingConfig, + } = useDevice(); -export const Security = () => { - const { config, setWorkingConfig, setDialogOpen } = useDevice(); const { removeError } = useAppStore(); const { t } = useTranslation("deviceConfig"); - const [keyState, setKeyState] = useState(() => ({ - publicKey: fromByteArray(config?.security?.publicKey ?? new Uint8Array(0)), - privateKey: fromByteArray( - config?.security?.privateKey ?? new Uint8Array(0), - ), - privateKeyDialogOpen: false, - })); + const securityConfig = getEffectiveConfig("security"); + const defaultValues = { + ...securityConfig, + ...{ + privateKey: fromByteArray( + securityConfig?.privateKey ?? new Uint8Array(0), + ), + publicKey: fromByteArray( + securityConfig?.publicKey ?? new Uint8Array(0), + ), + adminKey: [ + fromByteArray( + securityConfig?.adminKey?.at(0) ?? new Uint8Array(0), + ), + fromByteArray( + securityConfig?.adminKey?.at(1) ?? new Uint8Array(0), + ), + fromByteArray( + securityConfig?.adminKey?.at(2) ?? new Uint8Array(0), + ), + ], + }, + }; + + const formMethods = useForm({ + mode: "onChange", + defaultValues: defaultValues as DefaultValues, + resolver: createZodResolver(RawSecuritySchema), + shouldFocusError: false, + resetOptions: { keepDefaultValues: true }, + }); + const { setValue, formState } = formMethods; + + useEffect(() => { + onFormInit?.(formMethods); + }, [onFormInit, formMethods]); + + const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = useState( + false, + ); + const [managedModeDialogOpen, setManagedModeDialogOpen] = useState( + false, + ); const onSubmit = (data: RawSecurity) => { + if (!formState.isReady) return; + const payload: ParsedSecurity = { ...data, - privateKey: toByteArray(keyState.privateKey), - publicKey: toByteArray(keyState.publicKey), + privateKey: toByteArray(data.privateKey), + publicKey: toByteArray(data.publicKey), adminKey: [ toByteArray(data.adminKey.at(0) ?? ""), toByteArray(data.adminKey.at(1) ?? ""), @@ -45,6 +94,11 @@ export const Security = () => { ], }; + if (deepCompareConfig(config.security, payload, true)) { + removeWorkingConfig("security"); + return; + } + setWorkingConfig( create(Protobuf.Config.ConfigSchema, { payloadVariant: { @@ -54,18 +108,10 @@ export const Security = () => { }), ); }; + const pkiRegenerate = () => { const privateKey = getX25519PrivateKey(); - updatePublicKey(fromByteArray(privateKey)); - - setKeyState((prev) => ({ - ...prev, - privateKey: fromByteArray(privateKey), - privateKeyDialogOpen: false, - })); - - removeError("privateKey"); }; const updatePublicKey = (privateKey: string) => { @@ -73,18 +119,14 @@ export const Security = () => { const publicKey = fromByteArray( getX25519PublicKey(toByteArray(privateKey)), ); - setKeyState((prev) => ({ - ...prev, - privateKey: privateKey, - publicKey: publicKey, - })); + setValue("privateKey", privateKey); + setValue("publicKey", publicKey); + removeError("privateKey"); removeError("publicKey"); + setPrivateKeyDialogOpen(false); } catch (_e) { - setKeyState((prev) => ({ - ...prev, - privateKey: privateKey, - })); + setValue("privateKey", privateKey); } }; @@ -99,31 +141,9 @@ export const Security = () => { return ( <> + propMethods={formMethods} onSubmit={onSubmit} - validationSchema={RawSecuritySchema} formId="Config_SecurityConfig" - defaultValues={{ - ...config.security, - ...{ - privateKey: fromByteArray( - config?.security?.privateKey ?? new Uint8Array(0), - ), - publicKey: fromByteArray( - config?.security?.publicKey ?? new Uint8Array(0), - ), - adminKey: [ - fromByteArray( - config?.security?.adminKey.at(0) ?? new Uint8Array(0), - ), - fromByteArray( - config?.security?.adminKey.at(1) ?? new Uint8Array(0), - ), - fromByteArray( - config?.security?.adminKey.at(2) ?? new Uint8Array(0), - ), - ], - }, - }} fieldGroups={[ { label: t("security.title"), @@ -144,11 +164,7 @@ export const Security = () => { actionButtons: [ { text: t("button.generate"), - onClick: () => - setKeyState((prev) => ({ - ...prev, - privateKeyDialogOpen: true, - })), + onClick: () => setPrivateKeyDialogOpen(true), variant: "success", }, { @@ -160,8 +176,6 @@ export const Security = () => { properties: { showCopyButton: true, showPasswordToggle: true, - - value: keyState.privateKey, }, }, { @@ -172,7 +186,6 @@ export const Security = () => { description: t("security.publicKey.description"), properties: { showCopyButton: true, - value: keyState.publicKey, }, }, ], @@ -240,6 +253,13 @@ export const Security = () => { name: "isManaged", label: t("security.managed.label"), description: t("security.managed.description"), + inputChange: (checked) => { + if (checked) { + setManagedModeDialogOpen(true); + } + + setValue("isManaged", false); + }, }, { type: "toggle", @@ -275,14 +295,19 @@ export const Security = () => { title: t("pkiRegenerate.title"), description: t("pkiRegenerate.description"), }} - open={keyState.privateKeyDialogOpen} - onOpenChange={() => - setKeyState((prev) => ({ - ...prev, - privateKeyDialogOpen: false, - }))} + open={privateKeyDialogOpen} + onOpenChange={() => setPrivateKeyDialogOpen((prev) => !prev)} onSubmit={pkiRegenerate} /> + + setManagedModeDialogOpen((prev) => !prev)} + onSubmit={() => { + setValue("isManaged", true); + setManagedModeDialogOpen(false); + }} + /> ); }; diff --git a/src/components/PageComponents/Connect/BLE.tsx b/packages/web/src/components/PageComponents/Connect/BLE.tsx similarity index 98% rename from src/components/PageComponents/Connect/BLE.tsx rename to packages/web/src/components/PageComponents/Connect/BLE.tsx index a3458792..ca1dd326 100644 --- a/src/components/PageComponents/Connect/BLE.tsx +++ b/packages/web/src/components/PageComponents/Connect/BLE.tsx @@ -77,7 +77,7 @@ export const BLE = ( if (exists === -1) { setBleDevices(bleDevices.concat(device)); } - }).catch((error) => { + }).catch((error: Error) => { console.error("Error requesting device:", error); setConnectionInProgress(false); }).finally(() => { diff --git a/src/components/PageComponents/Connect/HTTP.test.tsx b/packages/web/src/components/PageComponents/Connect/HTTP.test.tsx similarity index 100% rename from src/components/PageComponents/Connect/HTTP.test.tsx rename to packages/web/src/components/PageComponents/Connect/HTTP.test.tsx diff --git a/src/components/PageComponents/Connect/HTTP.tsx b/packages/web/src/components/PageComponents/Connect/HTTP.tsx similarity index 100% rename from src/components/PageComponents/Connect/HTTP.tsx rename to packages/web/src/components/PageComponents/Connect/HTTP.tsx diff --git a/src/components/PageComponents/Connect/Serial.tsx b/packages/web/src/components/PageComponents/Connect/Serial.tsx similarity index 92% rename from src/components/PageComponents/Connect/Serial.tsx rename to packages/web/src/components/PageComponents/Connect/Serial.tsx index 9b793f38..ba6ee85c 100644 --- a/src/components/PageComponents/Connect/Serial.tsx +++ b/packages/web/src/components/PageComponents/Connect/Serial.tsx @@ -9,7 +9,7 @@ import { MeshDevice } from "@meshtastic/core"; import { TransportWebSerial } from "@meshtastic/transport-web-serial"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useMessageStore } from "../../../core/stores/messageStore/index.ts"; +import { useMessageStore } from "@core/stores/messageStore/index.ts"; export const Serial = ( { closeDialog }: TabElementProps, @@ -22,13 +22,13 @@ export const Serial = ( const { t } = useTranslation(); const updateSerialPortList = useCallback(async () => { - setSerialPorts(await navigator?.serial.getPorts()); + setSerialPorts(await navigator.serial.getPorts()); }, []); - navigator?.serial?.addEventListener("connect", () => { + navigator.serial.addEventListener("connect", () => { updateSerialPortList(); }); - navigator?.serial?.addEventListener("disconnect", () => { + navigator.serial.addEventListener("disconnect", () => { updateSerialPortList(); }); useEffect(() => { @@ -89,7 +89,7 @@ export const Serial = ( await navigator.serial.requestPort().then((port) => { setSerialPorts(serialPorts.concat(port)); // No need to setConnectionInProgress(false) here if requestPort is quick - }).catch((error) => { + }).catch((error: Error) => { console.error("Error requesting port:", error); }).finally(() => { setConnectionInProgress(false); diff --git a/src/components/PageComponents/Connect/Tabs/BluetoothTab.tsx b/packages/web/src/components/PageComponents/Connect/Tabs/BluetoothTab.tsx similarity index 100% rename from src/components/PageComponents/Connect/Tabs/BluetoothTab.tsx rename to packages/web/src/components/PageComponents/Connect/Tabs/BluetoothTab.tsx diff --git a/src/components/PageComponents/Connect/Tabs/HTTPTab.tsx b/packages/web/src/components/PageComponents/Connect/Tabs/HTTPTab.tsx similarity index 100% rename from src/components/PageComponents/Connect/Tabs/HTTPTab.tsx rename to packages/web/src/components/PageComponents/Connect/Tabs/HTTPTab.tsx diff --git a/src/components/PageComponents/Connect/Tabs/SerialTab.tsx b/packages/web/src/components/PageComponents/Connect/Tabs/SerialTab.tsx similarity index 100% rename from src/components/PageComponents/Connect/Tabs/SerialTab.tsx rename to packages/web/src/components/PageComponents/Connect/Tabs/SerialTab.tsx diff --git a/src/components/PageComponents/Map/NodeDetail.tsx b/packages/web/src/components/PageComponents/Map/NodeDetail.tsx similarity index 100% rename from src/components/PageComponents/Map/NodeDetail.tsx rename to packages/web/src/components/PageComponents/Map/NodeDetail.tsx diff --git a/src/components/PageComponents/Messages/ChannelChat.tsx b/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx similarity index 100% rename from src/components/PageComponents/Messages/ChannelChat.tsx rename to packages/web/src/components/PageComponents/Messages/ChannelChat.tsx diff --git a/src/components/PageComponents/Messages/MessageActionsMenu.tsx b/packages/web/src/components/PageComponents/Messages/MessageActionsMenu.tsx similarity index 100% rename from src/components/PageComponents/Messages/MessageActionsMenu.tsx rename to packages/web/src/components/PageComponents/Messages/MessageActionsMenu.tsx diff --git a/src/components/PageComponents/Messages/MessageInput.test.tsx b/packages/web/src/components/PageComponents/Messages/MessageInput.test.tsx similarity index 91% rename from src/components/PageComponents/Messages/MessageInput.test.tsx rename to packages/web/src/components/PageComponents/Messages/MessageInput.test.tsx index 2c88c1cc..0ddb8531 100644 --- a/src/components/PageComponents/Messages/MessageInput.test.tsx +++ b/packages/web/src/components/PageComponents/Messages/MessageInput.test.tsx @@ -86,7 +86,7 @@ describe("MessageInput", () => { it("should render the input field, byte counter, and send button", () => { renderComponent(); - expect(screen.getByPlaceholderText("Enter Message")).toBeInTheDocument(); + expect(screen.getByTestId("message-input-field")).toBeInTheDocument(); expect(screen.getByTestId("byte-counter")).toBeInTheDocument(); expect(screen.getByRole("button")).toBeInTheDocument(); expect(screen.getByTestId("send-icon")).toBeInTheDocument(); @@ -100,10 +100,6 @@ describe("MessageInput", () => { renderComponent(); - const inputElement = screen.getByPlaceholderText( - "Enter Message", - ) as HTMLInputElement; - expect(inputElement.value).toBe(initialDraft); expect(mockGetDraft).toHaveBeenCalledWith(defaultProps.to); const expectedBytes = new Blob([initialDraft]).size; expect(screen.getByTestId("byte-counter")).toHaveTextContent( @@ -113,7 +109,7 @@ describe("MessageInput", () => { it("should update input value, byte counter, and call setDraft on change within limits", () => { renderComponent(); - const inputElement = screen.getByPlaceholderText("Enter Message"); + const inputElement = screen.getByTestId("message-input-field"); const testMessage = "Hello there!"; const expectedBytes = new Blob([testMessage]).size; @@ -130,7 +126,7 @@ describe("MessageInput", () => { it("should NOT update input value or call setDraft if maxBytes is exceeded", () => { const smallMaxBytes = 5; renderComponent({ maxBytes: smallMaxBytes }); - const inputElement = screen.getByPlaceholderText("Enter Message"); + const inputElement = screen.getByTestId("message-input-field"); const initialValue = "12345"; const excessiveValue = "123456"; @@ -150,7 +146,7 @@ describe("MessageInput", () => { it("should call onSend, clear input, reset byte counter, and call clearDraft on valid submit", async () => { renderComponent(); - const inputElement = screen.getByPlaceholderText("Enter Message"); + const inputElement = screen.getByTestId("message-input-field"); const formElement = screen.getByRole("form"); const testMessage = "Send this message"; @@ -171,7 +167,7 @@ describe("MessageInput", () => { it("should trim whitespace before calling onSend", async () => { renderComponent(); - const inputElement = screen.getByPlaceholderText("Enter Message"); + const inputElement = screen.getByTestId("message-input-field"); const formElement = screen.getByRole("form"); const testMessageWithWhitespace = " Trim me! "; const expectedTrimmedMessage = "Trim me!"; @@ -190,7 +186,7 @@ describe("MessageInput", () => { it("should not call onSend or clearDraft if input is empty on submit", async () => { renderComponent(); - const inputElement = screen.getByPlaceholderText("Enter Message"); + const inputElement = screen.getByTestId("message-input-field"); const formElement = screen.getByRole("form"); expect((inputElement as HTMLInputElement).value).toBe(""); @@ -236,10 +232,14 @@ describe("MessageInput", () => { expect(mockGetDraft).toHaveBeenCalledWith(broadcastDest); expect( - (screen.getByPlaceholderText("Enter Message") as HTMLInputElement).value, + (screen.getByTestId( + "message-input-field", + ) as HTMLInputElement).value, ).toBe("Broadcast draft"); - const inputElement = screen.getByPlaceholderText("Enter Message"); + const inputElement = screen.getByTestId( + "message-input-field", + ) as HTMLInputElement; const formElement = screen.getByRole("form"); const newMessage = "New broadcast msg"; diff --git a/src/components/PageComponents/Messages/MessageInput.tsx b/packages/web/src/components/PageComponents/Messages/MessageInput.tsx similarity index 94% rename from src/components/PageComponents/Messages/MessageInput.tsx rename to packages/web/src/components/PageComponents/Messages/MessageInput.tsx index 3488e0a0..fb58bc3e 100644 --- a/src/components/PageComponents/Messages/MessageInput.tsx +++ b/packages/web/src/components/PageComponents/Messages/MessageInput.tsx @@ -4,6 +4,7 @@ import type { Types } from "@meshtastic/core"; import { SendIcon } from "lucide-react"; import { startTransition, useState } from "react"; import { useMessageStore } from "@core/stores/messageStore/index.ts"; +import { useTranslation } from "react-i18next"; export interface MessageInputProps { onSend: (message: string) => void; @@ -17,6 +18,7 @@ export const MessageInput = ({ maxBytes, }: MessageInputProps) => { const { setDraft, getDraft, clearDraft } = useMessageStore(); + const { t } = useTranslation("messages"); const calculateBytes = (text: string) => new Blob([text]).size; @@ -59,7 +61,7 @@ export const MessageInput = ({ autoFocus minLength={1} name="messageInput" - placeholder="Enter Message" + placeholder={t("sendMessage.placeholder")} autoComplete="off" value={localDraft} onChange={handleInputChange} diff --git a/src/components/PageComponents/Messages/MessageItem.tsx b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx similarity index 90% rename from src/components/PageComponents/Messages/MessageItem.tsx rename to packages/web/src/components/PageComponents/Messages/MessageItem.tsx index 7a932a1d..ee38e674 100644 --- a/src/components/PageComponents/Messages/MessageItem.tsx +++ b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx @@ -51,26 +51,26 @@ interface MessageItemProps { export const MessageItem = ({ message }: MessageItemProps) => { const { getNode } = useDevice(); const { getMyNodeNum } = useMessageStore(); - const { t, i18n } = useTranslation(); + const { t, i18n } = useTranslation("messages"); const MESSAGE_STATUS_MAP = useMemo( (): Record => ({ [MessageState.Ack]: { - displayText: t("message_item_status_delivered_displayText"), + displayText: t("deliveryStatus.delivered.displayText"), icon: CheckCircle2, - ariaLabel: t("message_item_status_delivered_ariaLabel"), + ariaLabel: t("deliveryStatus.delivered.label"), iconClassName: "text-green-500", }, [MessageState.Waiting]: { - displayText: t("message_item_status_waiting_displayText"), + displayText: t("deliveryStatus.waiting.displayText"), icon: CircleEllipsis, - ariaLabel: t("message_item_status_waiting_ariaLabel"), + ariaLabel: t("deliveryStatus.waiting.label"), iconClassName: "text-slate-400", }, [MessageState.Failed]: { - displayText: t("message_item_status_failed_displayText"), + displayText: t("deliveryStatus.failed.displayText"), icon: AlertCircle, - ariaLabel: t("message_item_status_failed_ariaLabel"), + ariaLabel: t("deliveryStatus.failed.label"), iconClassName: "text-red-500 dark:text-red-400", }, }), @@ -78,9 +78,9 @@ export const MessageItem = ({ message }: MessageItemProps) => { ); const UNKNOWN_STATUS = useMemo((): MessageStatusInfo => ({ - displayText: t("message_item_status_unknown_displayText"), + displayText: t("deliveryStatus.unknown.displayText"), icon: AlertCircle, - ariaLabel: t("message_item_status_unknown_ariaLabel"), + ariaLabel: t("deliveryStatus.unknown.label"), iconClassName: "text-red-500 dark:text-red-400", }), [t]); @@ -99,7 +99,7 @@ export const MessageItem = ({ message }: MessageItemProps) => { const { displayName, shortName, isFavorite } = useMemo(() => { const userIdHex = message.from.toString(16).toUpperCase().padStart(2, "0"); const last4 = userIdHex.slice(-4); - const fallbackName = t("message_item_fallbackName_withLastFour", { last4 }); + const fallbackName = t("fallbackName", { last4 }); const longName = messageUser?.user?.longName; const derivedShortName = messageUser?.user?.shortName || fallbackName; const derivedDisplayName = longName || derivedShortName; diff --git a/src/components/PageComponents/Messages/TraceRoute.test.tsx b/packages/web/src/components/PageComponents/Messages/TraceRoute.test.tsx similarity index 55% rename from src/components/PageComponents/Messages/TraceRoute.test.tsx rename to packages/web/src/components/PageComponents/Messages/TraceRoute.test.tsx index cfd9dc6a..9a0f7298 100644 --- a/src/components/PageComponents/Messages/TraceRoute.test.tsx +++ b/packages/web/src/components/PageComponents/Messages/TraceRoute.test.tsx @@ -2,29 +2,71 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; -import type { Protobuf } from "@meshtastic/core"; +import { mockDeviceStore } from "@core/stores/deviceStore.mock.ts"; +import { Protobuf } from "@meshtastic/core"; vi.mock("@core/stores/deviceStore"); describe("TraceRoute", () => { + const fromUser = { + user: { + $typeName: "meshtastic.User", + longName: "Source Node", + publicKey: new Uint8Array([1, 2, 3]), + shortName: "Source", + hwModel: 1, + macaddr: new Uint8Array([0x01, 0x02, 0x03, 0x04]), + id: "source-node", + isLicensed: false, + role: Protobuf.Config.Config_DeviceConfig_Role["CLIENT"], + } as Protobuf.Mesh.NodeInfo["user"], + }; + + const toUser = { + user: { + $typeName: "meshtastic.User", + longName: "Destination Node", + publicKey: new Uint8Array([4, 5, 6]), + shortName: "Destination", + hwModel: 2, + macaddr: new Uint8Array([0x05, 0x06, 0x07, 0x08]), + id: "destination-node", + isLicensed: false, + role: Protobuf.Config.Config_DeviceConfig_Role["CLIENT"], + } as Protobuf.Mesh.NodeInfo["user"], + }; + const mockNodes = new Map([ [ 1, - { num: 1, user: { longName: "Node A" } } as Protobuf.Mesh.NodeInfo, + { + num: 1, + user: { longName: "Node A", $typeName: "meshtastic.User" }, + $typeName: "meshtastic.NodeInfo", + } as Protobuf.Mesh.NodeInfo, ], [ 2, - { num: 2, user: { longName: "Node B" } } as Protobuf.Mesh.NodeInfo, + { + num: 2, + user: { longName: "Node B", $typeName: "meshtastic.User" }, + $typeName: "meshtastic.NodeInfo", + } as Protobuf.Mesh.NodeInfo, ], [ 3, - { num: 3, user: { longName: "Node C" } } as Protobuf.Mesh.NodeInfo, + { + num: 3, + user: { longName: "Node C", $typeName: "meshtastic.User" }, + $typeName: "meshtastic.NodeInfo", + } as Protobuf.Mesh.NodeInfo, ], ]); beforeEach(() => { vi.resetAllMocks(); vi.mocked(useDevice).mockReturnValue({ + ...mockDeviceStore, getNode: (nodeNum: number): Protobuf.Mesh.NodeInfo | undefined => { return mockNodes.get(nodeNum); }, @@ -34,16 +76,15 @@ describe("TraceRoute", () => { it("renders the route to destination with SNR values", () => { render( , ); - expect(screen.getAllByText("Source Node")).toHaveLength(1); + expect(screen.getByText("Source Node")).toBeInTheDocument(); expect(screen.getByText("Destination Node")).toBeInTheDocument(); - expect(screen.getByText("Node A")).toBeInTheDocument(); expect(screen.getByText("Node B")).toBeInTheDocument(); @@ -56,8 +97,8 @@ describe("TraceRoute", () => { it("renders the route back when provided", () => { render( { />, ); + // Check for the translated title expect(screen.getByText("Route back:")).toBeInTheDocument(); + // With route back, both names appear twice expect(screen.getAllByText("Source Node")).toHaveLength(2); - expect(screen.getAllByText("Destination Node")).toHaveLength(2); - expect(screen.getByText("Node C")).toBeInTheDocument(); expect(screen.getByText("Node A")).toBeInTheDocument(); - - expect(screen.getByText("↓ 35dBm")).toBeInTheDocument(); - expect(screen.getByText("↓ 45dBm")).toBeInTheDocument(); + expect(screen.getByText("Node C")).toBeInTheDocument(); expect(screen.getByText("↓ 15dBm")).toBeInTheDocument(); expect(screen.getByText("↓ 25dBm")).toBeInTheDocument(); + expect(screen.getByText("↓ 35dBm")).toBeInTheDocument(); + expect(screen.getByText("↓ 45dBm")).toBeInTheDocument(); }); it("renders '??' for missing SNR values", () => { render( , ); expect(screen.getByText("Node A")).toBeInTheDocument(); - expect(screen.getAllByText("↓ ??dBm")).toHaveLength(2); - }); - - it("renders hop hex if node is not found", () => { - render( - , - ); - - expect(screen.getByText("↓ 5dBm")).toBeInTheDocument(); - expect(screen.getByText("↓ 15dBm")).toBeInTheDocument(); + // Check for translated '??' placeholder + expect(screen.getAllByText(/↓ \?\?dBm/)).toHaveLength(2); }); }); diff --git a/src/components/PageComponents/Messages/TraceRoute.tsx b/packages/web/src/components/PageComponents/Messages/TraceRoute.tsx similarity index 82% rename from src/components/PageComponents/Messages/TraceRoute.tsx rename to packages/web/src/components/PageComponents/Messages/TraceRoute.tsx index 6ad25952..a1d85aa6 100644 --- a/src/components/PageComponents/Messages/TraceRoute.tsx +++ b/packages/web/src/components/PageComponents/Messages/TraceRoute.tsx @@ -3,9 +3,11 @@ import type { Protobuf } from "@meshtastic/core"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { useTranslation } from "react-i18next"; +type NodeUser = Pick; + export interface TraceRouteProps { - from?: Protobuf.Mesh.NodeInfo; - to?: Protobuf.Mesh.NodeInfo; + from: NodeUser; + to: NodeUser; route: Array; routeBack?: Array; snrTowards?: Array; @@ -14,14 +16,14 @@ export interface TraceRouteProps { interface RoutePathProps { title: string; - startNode?: Protobuf.Mesh.NodeInfo; - endNode?: Protobuf.Mesh.NodeInfo; + from: NodeUser; + to: NodeUser; path: number[]; snr?: number[]; } const RoutePath = ( - { title, startNode, endNode, path, snr }: RoutePathProps, + { title, from, to, path, snr }: RoutePathProps, ) => { const { getNode } = useDevice(); const { t } = useTranslation(); @@ -32,7 +34,7 @@ const RoutePath = ( className="ml-4 border-l-2 pl-2 border-l-slate-900 text-slate-900 dark:text-slate-100 dark:border-l-slate-100" >

{title}

-

{startNode?.user?.longName}

+

{from?.user?.longName}

↓ {snr?.[0] ?? t("unknown.num")} {t("unit.dbm")} @@ -49,7 +51,7 @@ const RoutePath = (

))} -

{endNode?.user?.longName}

+

{to?.user?.longName}

); }; @@ -67,16 +69,16 @@ export const TraceRoute = ({
{routeBack && routeBack.length > 0 && ( diff --git a/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx b/packages/web/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx similarity index 74% rename from src/components/PageComponents/ModuleConfig/AmbientLighting.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx index 51f9cc5d..50da8ec6 100644 --- a/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx @@ -4,15 +4,35 @@ import { AmbientLightingValidationSchema, } from "@app/validation/moduleConfig/ambientLighting.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface AmbientLightingModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const AmbientLighting = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const AmbientLighting = ( + { onFormInit }: AmbientLightingModuleConfigProps, +) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: AmbientLightingValidation) => { + if (deepCompareConfig(moduleConfig.ambientLighting, data, true)) { + removeWorkingModuleConfig("ambientLighting"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +46,11 @@ export const AmbientLighting = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={AmbientLightingValidationSchema} formId="ModuleConfig_AmbientLightingConfig" defaultValues={moduleConfig.ambientLighting} + values={getEffectiveModuleConfig("ambientLighting")} fieldGroups={[ { label: t("ambientLighting.title"), diff --git a/src/components/PageComponents/ModuleConfig/Audio.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Audio.tsx similarity index 79% rename from src/components/PageComponents/ModuleConfig/Audio.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/Audio.tsx index 4db01275..7a6ead8e 100644 --- a/src/components/PageComponents/ModuleConfig/Audio.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/Audio.tsx @@ -3,16 +3,34 @@ import { AudioValidationSchema, } from "@app/validation/moduleConfig/audio.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface AudioModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const Audio = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const Audio = ({ onFormInit }: AudioModuleConfigProps) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: AudioValidation) => { + if (deepCompareConfig(moduleConfig.audio, data, true)) { + removeWorkingModuleConfig("audio"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +44,11 @@ export const Audio = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={AudioValidationSchema} formId="ModuleConfig_AudioConfig" defaultValues={moduleConfig.audio} + values={getEffectiveModuleConfig("audio")} fieldGroups={[ { label: t("audio.title"), diff --git a/src/components/PageComponents/ModuleConfig/CannedMessage.tsx b/packages/web/src/components/PageComponents/ModuleConfig/CannedMessage.tsx similarity index 85% rename from src/components/PageComponents/ModuleConfig/CannedMessage.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/CannedMessage.tsx index 80221028..b0e075e8 100644 --- a/src/components/PageComponents/ModuleConfig/CannedMessage.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/CannedMessage.tsx @@ -3,16 +3,36 @@ import { CannedMessageValidationSchema, } from "@app/validation/moduleConfig/cannedMessage.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface CannedMessageModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const CannedMessage = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const CannedMessage = ( + { onFormInit }: CannedMessageModuleConfigProps, +) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: CannedMessageValidation) => { + if (deepCompareConfig(moduleConfig.cannedMessage, data, true)) { + removeWorkingModuleConfig("cannedMessage"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +46,11 @@ export const CannedMessage = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={CannedMessageValidationSchema} formId="ModuleConfig_CannedMessageConfig" defaultValues={moduleConfig.cannedMessage} + values={getEffectiveModuleConfig("cannedMessage")} fieldGroups={[ { label: t("cannedMessage.title"), diff --git a/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx b/packages/web/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx similarity index 84% rename from src/components/PageComponents/ModuleConfig/DetectionSensor.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx index 1ce0aaee..5d1fdb59 100644 --- a/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx @@ -4,15 +4,35 @@ import { DetectionSensorValidationSchema, } from "@app/validation/moduleConfig/detectionSensor.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface DetectionSensorModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const DetectionSensor = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const DetectionSensor = ( + { onFormInit }: DetectionSensorModuleConfigProps, +) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: DetectionSensorValidation) => { + if (deepCompareConfig(moduleConfig.detectionSensor, data, true)) { + removeWorkingModuleConfig("detectionSensor"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +46,11 @@ export const DetectionSensor = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={DetectionSensorValidationSchema} formId="ModuleConfig_DetectionSensorConfig" defaultValues={moduleConfig.detectionSensor} + values={getEffectiveModuleConfig("detectionSensor")} fieldGroups={[ { label: t("detectionSensor.title"), diff --git a/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx b/packages/web/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx similarity index 89% rename from src/components/PageComponents/ModuleConfig/ExternalNotification.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx index 61796342..ff3141ea 100644 --- a/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx @@ -3,16 +3,36 @@ import { ExternalNotificationValidationSchema, } from "@app/validation/moduleConfig/externalNotification.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface ExternalNotificationModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const ExternalNotification = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const ExternalNotification = ( + { onFormInit }: ExternalNotificationModuleConfigProps, +) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: ExternalNotificationValidation) => { + if (deepCompareConfig(moduleConfig.externalNotification, data, true)) { + removeWorkingModuleConfig("externalNotification"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +46,11 @@ export const ExternalNotification = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={ExternalNotificationValidationSchema} formId="ModuleConfig_ExternalNotificationConfig" defaultValues={moduleConfig.externalNotification} + values={getEffectiveModuleConfig("externalNotification")} fieldGroups={[ { label: t("externalNotification.title"), diff --git a/src/components/PageComponents/ModuleConfig/MQTT.tsx b/packages/web/src/components/PageComponents/ModuleConfig/MQTT.tsx similarity index 86% rename from src/components/PageComponents/ModuleConfig/MQTT.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/MQTT.tsx index e9587e89..6b6f4abd 100644 --- a/src/components/PageComponents/ModuleConfig/MQTT.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/MQTT.tsx @@ -4,37 +4,72 @@ import { MqttValidationSchema, } from "@app/validation/moduleConfig/mqtt.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface MqttModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const MQTT = () => { - const { config, moduleConfig, setWorkingModuleConfig } = useDevice(); +export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => { + const { + config, + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: MqttValidation) => { + const payload = { + ...data, + mapReportSettings: create( + Protobuf.ModuleConfig.ModuleConfig_MapReportSettingsSchema, + data.mapReportSettings, + ), + }; + + if (deepCompareConfig(moduleConfig.mqtt, payload, true)) { + removeWorkingModuleConfig("mqtt"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { case: "mqtt", - value: { - ...data, - mapReportSettings: create( - Protobuf.ModuleConfig.ModuleConfig_MapReportSettingsSchema, - data.mapReportSettings, - ), - }, + value: payload, }, }), ); }; + const populateDefaultValues = ( + cfg: Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined, + ) => { + return cfg + ? { + ...cfg, + mapReportSettings: cfg.mapReportSettings ?? + { publishIntervalSecs: 0, positionPrecision: 10 }, + } + : undefined; + }; + return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={MqttValidationSchema} formId="ModuleConfig_MqttConfig" - defaultValues={moduleConfig.mqtt} + defaultValues={populateDefaultValues(moduleConfig.mqtt)} + values={populateDefaultValues(getEffectiveModuleConfig("mqtt"))} fieldGroups={[ { label: t("mqtt.title"), diff --git a/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx b/packages/web/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx similarity index 70% rename from src/components/PageComponents/ModuleConfig/NeighborInfo.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx index eabb9207..389c70bc 100644 --- a/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx @@ -4,15 +4,33 @@ import { NeighborInfoValidationSchema, } from "@app/validation/moduleConfig/neighborInfo.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface NeighborInfoModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const NeighborInfo = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: NeighborInfoValidation) => { + if (deepCompareConfig(moduleConfig.neighborInfo, data, true)) { + removeWorkingModuleConfig("neighborInfo"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +44,11 @@ export const NeighborInfo = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={NeighborInfoValidationSchema} formId="ModuleConfig_NeighborInfoConfig" defaultValues={moduleConfig.neighborInfo} + values={getEffectiveModuleConfig("neighborInfo")} fieldGroups={[ { label: t("neighborInfo.title"), diff --git a/src/components/PageComponents/ModuleConfig/Paxcounter.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Paxcounter.tsx similarity index 77% rename from src/components/PageComponents/ModuleConfig/Paxcounter.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/Paxcounter.tsx index c11840ad..45921d73 100644 --- a/src/components/PageComponents/ModuleConfig/Paxcounter.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/Paxcounter.tsx @@ -3,16 +3,34 @@ import { PaxcounterValidationSchema, } from "@app/validation/moduleConfig/paxcounter.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface PaxcounterModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const Paxcounter = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: PaxcounterValidation) => { + if (deepCompareConfig(moduleConfig.paxcounter, data, true)) { + removeWorkingModuleConfig("paxcounter"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +44,11 @@ export const Paxcounter = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={PaxcounterValidationSchema} formId="ModuleConfig_PaxcounterConfig" defaultValues={moduleConfig.paxcounter} + values={getEffectiveModuleConfig("paxcounter")} fieldGroups={[ { label: t("paxcounter.title"), diff --git a/src/components/PageComponents/ModuleConfig/RangeTest.tsx b/packages/web/src/components/PageComponents/ModuleConfig/RangeTest.tsx similarity index 73% rename from src/components/PageComponents/ModuleConfig/RangeTest.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/RangeTest.tsx index 5960508d..fd18a110 100644 --- a/src/components/PageComponents/ModuleConfig/RangeTest.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/RangeTest.tsx @@ -3,16 +3,34 @@ import { RangeTestValidationSchema, } from "@app/validation/moduleConfig/rangeTest.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface RangeTestModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const RangeTest = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: RangeTestValidation) => { + if (deepCompareConfig(moduleConfig.rangeTest, data, true)) { + removeWorkingModuleConfig("rangeTest"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +44,11 @@ export const RangeTest = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={RangeTestValidationSchema} formId="ModuleConfig_RangeTestConfig" defaultValues={moduleConfig.rangeTest} + values={getEffectiveModuleConfig("rangeTest")} fieldGroups={[ { label: t("rangeTest.title"), diff --git a/src/components/PageComponents/ModuleConfig/Serial.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Serial.tsx similarity index 84% rename from src/components/PageComponents/ModuleConfig/Serial.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/Serial.tsx index d98684b5..9adcac16 100644 --- a/src/components/PageComponents/ModuleConfig/Serial.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/Serial.tsx @@ -3,16 +3,34 @@ import { SerialValidationSchema, } from "@app/validation/moduleConfig/serial.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface SerialModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const Serial = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const Serial = ({ onFormInit }: SerialModuleConfigProps) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: SerialValidation) => { + if (deepCompareConfig(moduleConfig.serial, data, true)) { + removeWorkingModuleConfig("serial"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +44,11 @@ export const Serial = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={SerialValidationSchema} formId="ModuleConfig_SerialConfig" defaultValues={moduleConfig.serial} + values={getEffectiveModuleConfig("serial")} fieldGroups={[ { label: t("serial.title"), diff --git a/src/components/PageComponents/ModuleConfig/StoreForward.tsx b/packages/web/src/components/PageComponents/ModuleConfig/StoreForward.tsx similarity index 79% rename from src/components/PageComponents/ModuleConfig/StoreForward.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/StoreForward.tsx index 02b5d630..155c235a 100644 --- a/src/components/PageComponents/ModuleConfig/StoreForward.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/StoreForward.tsx @@ -3,16 +3,34 @@ import { StoreForwardValidationSchema, } from "@app/validation/moduleConfig/storeForward.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface StoreForwardModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const StoreForward = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: StoreForwardValidation) => { + if (deepCompareConfig(moduleConfig.storeForward, data, true)) { + removeWorkingModuleConfig("storeForward"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +44,11 @@ export const StoreForward = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={StoreForwardValidationSchema} formId="ModuleConfig_StoreForwardConfig" defaultValues={moduleConfig.storeForward} + values={getEffectiveModuleConfig("storeForward")} fieldGroups={[ { label: t("storeForward.title"), diff --git a/src/components/PageComponents/ModuleConfig/Telemetry.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx similarity index 84% rename from src/components/PageComponents/ModuleConfig/Telemetry.tsx rename to packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx index d296fe71..0bb71134 100644 --- a/src/components/PageComponents/ModuleConfig/Telemetry.tsx +++ b/packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx @@ -3,16 +3,34 @@ import { TelemetryValidationSchema, } from "@app/validation/moduleConfig/telemetry.ts"; import { create } from "@bufbuild/protobuf"; -import { DynamicForm } from "@components/Form/DynamicForm.tsx"; +import { + DynamicForm, + type DynamicFormFormInit, +} from "@components/Form/DynamicForm.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { Protobuf } from "@meshtastic/core"; import { useTranslation } from "react-i18next"; +import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; + +interface TelemetryModuleConfigProps { + onFormInit: DynamicFormFormInit; +} -export const Telemetry = () => { - const { moduleConfig, setWorkingModuleConfig } = useDevice(); +export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => { + const { + moduleConfig, + setWorkingModuleConfig, + getEffectiveModuleConfig, + removeWorkingModuleConfig, + } = useDevice(); const { t } = useTranslation("moduleConfig"); const onSubmit = (data: TelemetryValidation) => { + if (deepCompareConfig(moduleConfig.telemetry, data, true)) { + removeWorkingModuleConfig("telemetry"); + return; + } + setWorkingModuleConfig( create(Protobuf.ModuleConfig.ModuleConfigSchema, { payloadVariant: { @@ -26,9 +44,11 @@ export const Telemetry = () => { return ( onSubmit={onSubmit} + onFormInit={onFormInit} validationSchema={TelemetryValidationSchema} formId="ModuleConfig_TelemetryConfig" defaultValues={moduleConfig.telemetry} + values={getEffectiveModuleConfig("telemetry")} fieldGroups={[ { label: t("telemetry.title"), diff --git a/src/components/PageLayout.tsx b/packages/web/src/components/PageLayout.tsx similarity index 64% rename from src/components/PageLayout.tsx rename to packages/web/src/components/PageLayout.tsx index a08a2afa..7a8c1eee 100644 --- a/src/components/PageLayout.tsx +++ b/packages/web/src/components/PageLayout.tsx @@ -8,12 +8,14 @@ import { ErrorPage } from "@components/UI/ErrorPage.tsx"; export interface ActionItem { key: string; - icon: LucideIcon; + icon?: LucideIcon; iconClasses?: string; onClick: () => void; disabled?: boolean; isLoading?: boolean; ariaLabel?: string; + label?: string; + className?: string; } export interface PageLayoutProps { @@ -69,27 +71,38 @@ export const PageLayout = ({ {label} -
- {actions?.map((action) => ( - - ))} + onClick={action.onClick} + aria-label={action.ariaLabel || `Action ${action.key}`} + aria-disabled={action.disabled} + aria-busy={action.isLoading} + > + {action.icon && + (action.isLoading ? : ( + + ))} + {action.label && ( + + {action.label} + + )} + + ); + })}
diff --git a/src/components/Sidebar.tsx b/packages/web/src/components/Sidebar.tsx similarity index 94% rename from src/components/Sidebar.tsx rename to packages/web/src/components/Sidebar.tsx index a55d89e8..ddef36aa 100644 --- a/src/components/Sidebar.tsx +++ b/packages/web/src/components/Sidebar.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState, useTransition } from "react"; +import type React from "react"; +import { useEffect, useState, useTransition } from "react"; import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; @@ -101,7 +102,7 @@ export const Sidebar = ({ children }: SidebarProps) => { setDisplayedNodeCount(currentNodeCountValue); }); } - }, [currentNodeCountValue, displayedNodeCount, startNodeCountTransition]); + }, [currentNodeCountValue, displayedNodeCount]); const pages: NavLink[] = [ { @@ -146,7 +147,7 @@ export const Sidebar = ({ children }: SidebarProps) => { )} > {t("app.logo")} @@ -163,10 +164,7 @@ export const Sidebar = ({ children }: SidebarProps) => {
- + {pages.map((link) => { return ( { })} -
+
{children}
diff --git a/src/components/ThemeSwitcher.tsx b/packages/web/src/components/ThemeSwitcher.tsx similarity index 100% rename from src/components/ThemeSwitcher.tsx rename to packages/web/src/components/ThemeSwitcher.tsx diff --git a/src/components/Toaster.tsx b/packages/web/src/components/Toaster.tsx similarity index 100% rename from src/components/Toaster.tsx rename to packages/web/src/components/Toaster.tsx diff --git a/src/components/UI/Accordion.tsx b/packages/web/src/components/UI/Accordion.tsx similarity index 100% rename from src/components/UI/Accordion.tsx rename to packages/web/src/components/UI/Accordion.tsx diff --git a/src/components/UI/Avatar.tsx b/packages/web/src/components/UI/Avatar.tsx similarity index 96% rename from src/components/UI/Avatar.tsx rename to packages/web/src/components/UI/Avatar.tsx index f9b0054d..f4192d64 100644 --- a/src/components/UI/Avatar.tsx +++ b/packages/web/src/components/UI/Avatar.tsx @@ -114,7 +114,7 @@ export const Avatar = ({ /> - Favorite + {t("nodeDetail.favorite.label", { ns: "nodes" })} @@ -132,7 +132,7 @@ export const Avatar = ({ /> - Node error + {t("nodeDetail.error.label", { ns: "nodes" })} diff --git a/src/components/UI/Button.tsx b/packages/web/src/components/UI/Button.tsx similarity index 70% rename from src/components/UI/Button.tsx rename to packages/web/src/components/UI/Button.tsx index ef5ab101..905eca30 100644 --- a/src/components/UI/Button.tsx +++ b/packages/web/src/components/UI/Button.tsx @@ -46,44 +46,36 @@ export interface ButtonProps iconAlignment?: "left" | "right"; } -const Button = React.forwardRef( - ( - { - className, - variant, - size, - disabled, - icon, - iconAlignment = "left", - children, - ...props - }, - ref, - ) => { - return ( - - ); - }, -); -Button.displayName = "Button"; +const Button = ({ + className, + variant, + size, + disabled, + icon, + iconAlignment = "left", + children, + ...props +}: ButtonProps) => { + return ( + + ); +}; export { Button, buttonVariants }; diff --git a/src/components/UI/Checkbox/Checkbox.test.tsx b/packages/web/src/components/UI/Checkbox/Checkbox.test.tsx similarity index 90% rename from src/components/UI/Checkbox/Checkbox.test.tsx rename to packages/web/src/components/UI/Checkbox/Checkbox.test.tsx index 7e5ba73d..e7badabe 100644 --- a/src/components/UI/Checkbox/Checkbox.test.tsx +++ b/packages/web/src/components/UI/Checkbox/Checkbox.test.tsx @@ -23,18 +23,6 @@ vi.mock("@components/UI/Label.tsx", () => ({ ), })); -vi.mock("@core/utils/cn.ts", () => ({ - cn: (...args) => args.filter(Boolean).join(" "), -})); - -vi.mock("react", async () => { - const actual = await vi.importActual("react"); - return { - ...actual, - useId: () => "test-id", - }; -}); - describe("Checkbox", () => { beforeEach(cleanup); @@ -67,11 +55,6 @@ describe("Checkbox", () => { expect(screen.getByRole("checkbox").id).toBe("custom-id"); }); - it("generates id when not provided", () => { - render(); - expect(screen.getByRole("checkbox").id).toBe("test-id"); - }); - it("renders children in Label component", () => { render(Test Label); expect(screen.getByTestId("label-component")).toHaveTextContent( diff --git a/src/components/UI/Checkbox/index.tsx b/packages/web/src/components/UI/Checkbox/index.tsx similarity index 100% rename from src/components/UI/Checkbox/index.tsx rename to packages/web/src/components/UI/Checkbox/index.tsx diff --git a/src/components/UI/Command.tsx b/packages/web/src/components/UI/Command.tsx similarity index 100% rename from src/components/UI/Command.tsx rename to packages/web/src/components/UI/Command.tsx diff --git a/src/components/UI/Dialog.tsx b/packages/web/src/components/UI/Dialog.tsx similarity index 97% rename from src/components/UI/Dialog.tsx rename to packages/web/src/components/UI/Dialog.tsx index 763e53e0..8a820077 100644 --- a/src/components/UI/Dialog.tsx +++ b/packages/web/src/components/UI/Dialog.tsx @@ -58,9 +58,7 @@ DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogClose = ({ className, ...props -}: DialogPrimitive.DialogCloseProps & React.RefAttributes & { - className?: string; -}) => ( +}: React.ComponentPropsWithoutRef) => ( Chirpy the Meshtastic error diff --git a/src/components/UI/Footer.tsx b/packages/web/src/components/UI/Footer.tsx similarity index 100% rename from src/components/UI/Footer.tsx rename to packages/web/src/components/UI/Footer.tsx diff --git a/src/components/UI/Generator.tsx b/packages/web/src/components/UI/Generator.tsx similarity index 82% rename from src/components/UI/Generator.tsx rename to packages/web/src/components/UI/Generator.tsx index 2cb22b16..a2098037 100644 --- a/src/components/UI/Generator.tsx +++ b/packages/web/src/components/UI/Generator.tsx @@ -8,6 +8,7 @@ import { SelectTrigger, SelectValue, } from "@components/UI/Select.tsx"; +import { useTranslation } from "react-i18next"; export interface ActionButton { text: string; @@ -22,7 +23,7 @@ export interface GeneratorProps extends React.BaseHTMLAttributes { devicePSKBitCount?: number; value: string; id: string; - variant: "default" | "invalid"; + variant: "default" | "invalid" | "dirty"; actionButtons: ActionButton[]; bits?: { text: string; value: string; key: string }[]; selectChange: (event: string) => void; @@ -40,12 +41,7 @@ const Generator = ( variant, value, actionButtons, - bits = [ - { text: "256 bit", value: "32", key: "bit256" }, - { text: "128 bit", value: "16", key: "bit128" }, - { text: "8 bit", value: "1", key: "bit8" }, - { text: "Empty", value: "0", key: "empty" }, - ], + bits, selectChange, inputChange, disabled, @@ -55,6 +51,30 @@ const Generator = ( }: GeneratorProps, ) => { const inputRef = useRef(null); + const { t } = useTranslation(); + + const passwordRequiredBitSize = bits ? bits : [ + { + text: t("security.256bit"), + value: "32", + key: "bit256", + }, + { + text: t("security.128bit"), + value: "16", + key: "bit128", + }, + { + text: t("security.8bit"), + value: "1", + key: "bit8", + }, + { + text: t("security.0bit"), + value: "0", + key: "bit0", + }, + ]; // Invokes onChange event on the input element when the value changes from the parent component useEffect(() => { @@ -91,7 +111,7 @@ const Generator = ( - {bits.map(({ text, value, key }) => ( + {passwordRequiredBitSize.map(({ text, value, key }) => ( {text} diff --git a/src/components/UI/Input.tsx b/packages/web/src/components/UI/Input.tsx similarity index 88% rename from src/components/UI/Input.tsx rename to packages/web/src/components/UI/Input.tsx index 81a072e7..faa24544 100644 --- a/src/components/UI/Input.tsx +++ b/packages/web/src/components/UI/Input.tsx @@ -6,14 +6,17 @@ import { useCopyToClipboard } from "@core/hooks/useCopyToClipboard.ts"; import { usePasswordVisibilityToggle } from "@core/hooks/usePasswordVisibilityToggle.ts"; import { useTranslation } from "react-i18next"; +const cnInvalidBase = "border-2 border-red-500 dark:border-red-500"; +const cnDirtyBase = "border-2 border-sky-500 dark:border-sky-500"; + const inputVariants = cva( "flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:bg-transparet dark:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-600", { variants: { variant: { default: "border-slate-300 dark:border-slate-500", - invalid: - "border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500", + invalid: `${cnInvalidBase} focus:ring-red-500 dark:focus:ring-red-500`, + dirty: `${cnDirtyBase} focus:ring-sky-500 dark:focus:ring-sky-500`, }, }, defaultVariants: { @@ -49,6 +52,7 @@ const Input = React.forwardRef( className, containerClassName, variant, + disabled, type = "text", prefix, suffix, @@ -137,6 +141,11 @@ const Input = React.forwardRef( className, ); + const extrasClassName = cn([ + variant === "invalid" && `${cnInvalidBase} border-l-0`, + variant === "dirty" && `${cnDirtyBase} border-l-0`, + ]); + return (
( ref={ref} value={value} onChange={onChange} + disabled={disabled} {...props} /> @@ -161,6 +171,7 @@ const Input = React.forwardRef( @@ -171,7 +182,10 @@ const Input = React.forwardRef( {hasActions && (
( key={action.id} type="button" className={cn( - "inline-flex h-full items-center justify-center px-2.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus:ring-1 focus:ring-slate-400 focus:ring-offset-0 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200 dark:focus:ring-slate-500 hover:rounded-md dark:hover:rounded-md", + "inline-flex h-full items-center justify-center px-2.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus:ring-1 focus:ring-slate-400 focus:ring-offset-0 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200 dark:focus:ring-slate-500 last:hover:rounded-r-md last:dark:hover:rounded-r-md", + disabled && "text-slate-300 dark:text-slate-600", action.id === "copy-value" && isCopied && "text-green-600 dark:text-green-500", )} diff --git a/src/components/UI/Label.tsx b/packages/web/src/components/UI/Label.tsx similarity index 100% rename from src/components/UI/Label.tsx rename to packages/web/src/components/UI/Label.tsx diff --git a/src/components/UI/Menubar.tsx b/packages/web/src/components/UI/Menubar.tsx similarity index 100% rename from src/components/UI/Menubar.tsx rename to packages/web/src/components/UI/Menubar.tsx diff --git a/src/components/UI/MultiSelect.tsx b/packages/web/src/components/UI/MultiSelect.tsx similarity index 100% rename from src/components/UI/MultiSelect.tsx rename to packages/web/src/components/UI/MultiSelect.tsx diff --git a/src/components/UI/Popover.tsx b/packages/web/src/components/UI/Popover.tsx similarity index 100% rename from src/components/UI/Popover.tsx rename to packages/web/src/components/UI/Popover.tsx diff --git a/src/components/UI/ScrollArea.tsx b/packages/web/src/components/UI/ScrollArea.tsx similarity index 100% rename from src/components/UI/ScrollArea.tsx rename to packages/web/src/components/UI/ScrollArea.tsx diff --git a/src/components/UI/Select.tsx b/packages/web/src/components/UI/Select.tsx similarity index 100% rename from src/components/UI/Select.tsx rename to packages/web/src/components/UI/Select.tsx diff --git a/src/components/UI/Seperator.tsx b/packages/web/src/components/UI/Seperator.tsx similarity index 100% rename from src/components/UI/Seperator.tsx rename to packages/web/src/components/UI/Seperator.tsx diff --git a/src/components/UI/Sidebar/SidebarButton.tsx b/packages/web/src/components/UI/Sidebar/SidebarButton.tsx similarity index 91% rename from src/components/UI/Sidebar/SidebarButton.tsx rename to packages/web/src/components/UI/Sidebar/SidebarButton.tsx index 920fb08c..8dc1aeca 100644 --- a/src/components/UI/Sidebar/SidebarButton.tsx +++ b/packages/web/src/components/UI/Sidebar/SidebarButton.tsx @@ -13,6 +13,7 @@ export interface SidebarButtonProps { onClick?: () => void; disabled?: boolean; preventCollapse?: boolean; + isDirty?: boolean; } export const SidebarButton = ({ @@ -24,6 +25,7 @@ export const SidebarButton = ({ onClick, disabled = false, preventCollapse = false, + isDirty, }: SidebarButtonProps) => { const { isCollapsed: isSidebarCollapsed } = useSidebar(); const isButtonCollapsed = isSidebarCollapsed && !preventCollapse; @@ -64,13 +66,14 @@ export const SidebarButton = ({ {label} - {!isButtonCollapsed && !active && count && count > 0 && ( + {!isButtonCollapsed && ((!active && count && count > 0) || isDirty) && (
{count} diff --git a/src/components/UI/Sidebar/SidebarSection.tsx b/packages/web/src/components/UI/Sidebar/SidebarSection.tsx similarity index 100% rename from src/components/UI/Sidebar/SidebarSection.tsx rename to packages/web/src/components/UI/Sidebar/SidebarSection.tsx diff --git a/src/components/UI/Slider.tsx b/packages/web/src/components/UI/Slider.tsx similarity index 100% rename from src/components/UI/Slider.tsx rename to packages/web/src/components/UI/Slider.tsx diff --git a/src/components/UI/Spinner.tsx b/packages/web/src/components/UI/Spinner.tsx similarity index 100% rename from src/components/UI/Spinner.tsx rename to packages/web/src/components/UI/Spinner.tsx diff --git a/src/components/UI/Switch.tsx b/packages/web/src/components/UI/Switch.tsx similarity index 100% rename from src/components/UI/Switch.tsx rename to packages/web/src/components/UI/Switch.tsx diff --git a/src/components/UI/Tabs.tsx b/packages/web/src/components/UI/Tabs.tsx similarity index 100% rename from src/components/UI/Tabs.tsx rename to packages/web/src/components/UI/Tabs.tsx diff --git a/src/components/UI/Toast.tsx b/packages/web/src/components/UI/Toast.tsx similarity index 100% rename from src/components/UI/Toast.tsx rename to packages/web/src/components/UI/Toast.tsx diff --git a/src/components/UI/ToggleGroup.tsx b/packages/web/src/components/UI/ToggleGroup.tsx similarity index 100% rename from src/components/UI/ToggleGroup.tsx rename to packages/web/src/components/UI/ToggleGroup.tsx diff --git a/src/components/UI/Tooltip.tsx b/packages/web/src/components/UI/Tooltip.tsx similarity index 100% rename from src/components/UI/Tooltip.tsx rename to packages/web/src/components/UI/Tooltip.tsx diff --git a/src/components/UI/Typography/Blockquote.tsx b/packages/web/src/components/UI/Typography/Blockquote.tsx similarity index 100% rename from src/components/UI/Typography/Blockquote.tsx rename to packages/web/src/components/UI/Typography/Blockquote.tsx diff --git a/src/components/UI/Typography/Code.tsx b/packages/web/src/components/UI/Typography/Code.tsx similarity index 100% rename from src/components/UI/Typography/Code.tsx rename to packages/web/src/components/UI/Typography/Code.tsx diff --git a/src/components/UI/Typography/Heading.tsx b/packages/web/src/components/UI/Typography/Heading.tsx similarity index 100% rename from src/components/UI/Typography/Heading.tsx rename to packages/web/src/components/UI/Typography/Heading.tsx diff --git a/src/components/UI/Typography/Link.tsx b/packages/web/src/components/UI/Typography/Link.tsx similarity index 80% rename from src/components/UI/Typography/Link.tsx rename to packages/web/src/components/UI/Typography/Link.tsx index de477ed9..00fe91bf 100644 --- a/src/components/UI/Typography/Link.tsx +++ b/packages/web/src/components/UI/Typography/Link.tsx @@ -6,7 +6,11 @@ import { export interface LinkProps extends RouterLinkProps { href: string; - children?: React.ReactNode; + children?: + | React.ReactNode + | (( + state: { isActive: boolean; isTransitioning: boolean }, + ) => React.ReactNode); className?: string; } diff --git a/src/components/UI/Typography/P.tsx b/packages/web/src/components/UI/Typography/P.tsx similarity index 100% rename from src/components/UI/Typography/P.tsx rename to packages/web/src/components/UI/Typography/P.tsx diff --git a/src/components/UI/Typography/Subtle.tsx b/packages/web/src/components/UI/Typography/Subtle.tsx similarity index 100% rename from src/components/UI/Typography/Subtle.tsx rename to packages/web/src/components/UI/Typography/Subtle.tsx diff --git a/src/components/generic/Blur.tsx b/packages/web/src/components/generic/Blur.tsx similarity index 100% rename from src/components/generic/Blur.tsx rename to packages/web/src/components/generic/Blur.tsx diff --git a/src/components/generic/DeviceImage.tsx b/packages/web/src/components/generic/DeviceImage.tsx similarity index 97% rename from src/components/generic/DeviceImage.tsx rename to packages/web/src/components/generic/DeviceImage.tsx index b80ebbac..ef96a429 100644 --- a/src/components/generic/DeviceImage.tsx +++ b/packages/web/src/components/generic/DeviceImage.tsx @@ -47,7 +47,7 @@ const hardwareModelToFilename: { [key: string]: string } = { }; export const DeviceImage = ({ deviceType, className }: DeviceImageProps) => { - const getPath = (device: string) => `/${device}`; + const getPath = (device: string) => `/devices/${device}`; const device = hardwareModelToFilename[deviceType] || "unknown.svg"; return {device}; }; diff --git a/src/components/generic/Filter/FilterComponents.tsx b/packages/web/src/components/generic/Filter/FilterComponents.tsx similarity index 100% rename from src/components/generic/Filter/FilterComponents.tsx rename to packages/web/src/components/generic/Filter/FilterComponents.tsx diff --git a/src/components/generic/Filter/FilterControl.tsx b/packages/web/src/components/generic/Filter/FilterControl.tsx similarity index 92% rename from src/components/generic/Filter/FilterControl.tsx rename to packages/web/src/components/generic/Filter/FilterControl.tsx index 87ff3723..f3c0c14c 100644 --- a/src/components/generic/Filter/FilterControl.tsx +++ b/packages/web/src/components/generic/Filter/FilterControl.tsx @@ -190,10 +190,12 @@ export function FilterControl({ ); const handleBoolChange = useCallback( - (key: K, value: string) => { - const typedValue = value === "" - ? undefined - : JSON.parse(value.toLowerCase()); + (key: K, value: string | boolean) => { + const typedValue = value === true || value === "true" + ? true + : value === false || value === "false" + ? false + : undefined; setFilterState((prev) => ({ ...prev, @@ -386,6 +388,28 @@ export function FilterControl({ formatEnumLabel(Protobuf.Mesh.HardwareModel[val])} /> + + + +
), diff --git a/packages/web/src/core/hooks/useLang.ts b/packages/web/src/core/hooks/useLang.ts new file mode 100644 index 00000000..e7a7e6aa --- /dev/null +++ b/packages/web/src/core/hooks/useLang.ts @@ -0,0 +1,63 @@ +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { + FALLBACK_LANGUAGE_CODE, + Lang, + LangCode, + supportedLanguages, +} from "@app/i18n-config.ts"; +import useLocalStorage from "./useLocalStorage.ts"; + +const STORAGE_KEY = "language"; + +type LanguageState = { + language: LangCode; +}; + +function useLang() { + const { i18n } = useTranslation(); + const [_, setLanguageInStorage] = useLocalStorage( + STORAGE_KEY, + null, + ); + + const currentLanguage = useMemo((): Lang | undefined => { + const lang = supportedLanguages.find((l) => l.code === i18n.language); + if (lang) { + return lang; + } + return supportedLanguages.find((l) => l.code === FALLBACK_LANGUAGE_CODE); + }, [i18n.language]); + + const collator = useMemo(() => { + return new Intl.Collator(i18n.language, { sensitivity: "base" }); + }, [i18n.language]); + + const set = useCallback( + async (lng: LangCode, persist = true) => { + if (i18n.language === lng) { + return; + } + try { + if (persist) { + setLanguageInStorage({ language: lng }); + } + await i18n.changeLanguage(lng); + } catch (e) { + console.warn("Failed to change language:", e); + } + }, + [i18n, setLanguageInStorage], + ); + + const compare = useCallback( + (a: string, b: string) => { + return collator.compare(a, b); + }, + [collator], + ); + + return { compare, set, currentLanguage }; +} + +export default useLang; diff --git a/src/core/hooks/useLocalStorage.test.ts b/packages/web/src/core/hooks/useLocalStorage.test.ts similarity index 100% rename from src/core/hooks/useLocalStorage.test.ts rename to packages/web/src/core/hooks/useLocalStorage.test.ts diff --git a/src/core/hooks/useLocalStorage.ts b/packages/web/src/core/hooks/useLocalStorage.ts similarity index 100% rename from src/core/hooks/useLocalStorage.ts rename to packages/web/src/core/hooks/useLocalStorage.ts diff --git a/src/core/hooks/usePasswordVisibilityToggle.test.ts b/packages/web/src/core/hooks/usePasswordVisibilityToggle.test.ts similarity index 100% rename from src/core/hooks/usePasswordVisibilityToggle.test.ts rename to packages/web/src/core/hooks/usePasswordVisibilityToggle.test.ts diff --git a/src/core/hooks/usePasswordVisibilityToggle.ts b/packages/web/src/core/hooks/usePasswordVisibilityToggle.ts similarity index 100% rename from src/core/hooks/usePasswordVisibilityToggle.ts rename to packages/web/src/core/hooks/usePasswordVisibilityToggle.ts diff --git a/src/core/hooks/usePinnedItems.ts b/packages/web/src/core/hooks/usePinnedItems.ts similarity index 100% rename from src/core/hooks/usePinnedItems.ts rename to packages/web/src/core/hooks/usePinnedItems.ts diff --git a/src/core/hooks/usePositionFlags.ts b/packages/web/src/core/hooks/usePositionFlags.ts similarity index 100% rename from src/core/hooks/usePositionFlags.ts rename to packages/web/src/core/hooks/usePositionFlags.ts diff --git a/src/core/hooks/useTheme.ts b/packages/web/src/core/hooks/useTheme.ts similarity index 100% rename from src/core/hooks/useTheme.ts rename to packages/web/src/core/hooks/useTheme.ts diff --git a/src/core/hooks/useToast.test.tsx b/packages/web/src/core/hooks/useToast.test.tsx similarity index 100% rename from src/core/hooks/useToast.test.tsx rename to packages/web/src/core/hooks/useToast.test.tsx diff --git a/src/core/hooks/useToast.ts b/packages/web/src/core/hooks/useToast.ts similarity index 100% rename from src/core/hooks/useToast.ts rename to packages/web/src/core/hooks/useToast.ts diff --git a/src/core/stores/appStore.ts b/packages/web/src/core/stores/appStore.ts similarity index 100% rename from src/core/stores/appStore.ts rename to packages/web/src/core/stores/appStore.ts diff --git a/packages/web/src/core/stores/deviceStore.mock.ts b/packages/web/src/core/stores/deviceStore.mock.ts new file mode 100644 index 00000000..43af9a9e --- /dev/null +++ b/packages/web/src/core/stores/deviceStore.mock.ts @@ -0,0 +1,89 @@ +import { vi } from "vitest"; +import { type Device } from "./deviceStore.ts"; +import { Protobuf } from "@meshtastic/core"; + +/** + * You can spread this base mock in your tests and override only the + * properties relevant to a specific test case. + * + * @example + * vi.mocked(useDevice).mockReturnValue({ + * ...mockDeviceStore, + * getNode: (nodeNum) => mockNodes.get(nodeNum), + * }); + */ +export const mockDeviceStore: Device = { + id: 0, + status: 5 as const, + channels: new Map(), + config: {} as Protobuf.LocalOnly.LocalConfig, + moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig, + workingConfig: [], + workingModuleConfig: [], + hardware: {} as Protobuf.Mesh.MyNodeInfo, + metadata: new Map(), + traceroutes: new Map(), + nodeErrors: new Map(), + connection: undefined, + activeNode: 0, + waypoints: [], + pendingSettingsChanges: false, + messageDraft: "", + unreadCounts: new Map(), + nodesMap: new Map(), + dialog: { + import: false, + QR: false, + shutdown: false, + reboot: false, + rebootOTA: false, + deviceName: false, + nodeRemoval: false, + pkiBackup: false, + nodeDetails: false, + unsafeRoles: false, + refreshKeys: false, + deleteMessages: false, + managedMode: false, + }, + setStatus: vi.fn(), + setConfig: vi.fn(), + setModuleConfig: vi.fn(), + setWorkingConfig: vi.fn(), + setWorkingModuleConfig: vi.fn(), + getWorkingConfig: vi.fn(), + getWorkingModuleConfig: vi.fn(), + removeWorkingConfig: vi.fn(), + removeWorkingModuleConfig: vi.fn(), + getEffectiveConfig: vi.fn(), + getEffectiveModuleConfig: vi.fn(), + setHardware: vi.fn(), + setActiveNode: vi.fn(), + setPendingSettingsChanges: vi.fn(), + addChannel: vi.fn(), + addWaypoint: vi.fn(), + addNodeInfo: vi.fn(), + addUser: vi.fn(), + addPosition: vi.fn(), + addConnection: vi.fn(), + addTraceRoute: vi.fn(), + addMetadata: vi.fn(), + removeNode: vi.fn(), + setDialogOpen: vi.fn(), + getDialogOpen: vi.fn().mockReturnValue(false), + processPacket: vi.fn(), + setMessageDraft: vi.fn(), + setNodeError: vi.fn(), + clearNodeError: vi.fn(), + getNodeError: vi.fn().mockReturnValue(undefined), + hasNodeError: vi.fn().mockReturnValue(false), + incrementUnread: vi.fn(), + resetUnread: vi.fn(), + getNodes: vi.fn().mockReturnValue([]), + getNodesLength: vi.fn().mockReturnValue(0), + getNode: vi.fn().mockReturnValue(undefined), + getMyNode: vi.fn(), + sendAdminMessage: vi.fn(), + updateFavorite: vi.fn(), + updateIgnored: vi.fn(), +}; diff --git a/src/core/stores/deviceStore.ts b/packages/web/src/core/stores/deviceStore.ts similarity index 76% rename from src/core/stores/deviceStore.ts rename to packages/web/src/core/stores/deviceStore.ts index d8b7b03d..21850517 100644 --- a/src/core/stores/deviceStore.ts +++ b/packages/web/src/core/stores/deviceStore.ts @@ -18,6 +18,14 @@ type NodeError = { node: number; error: string; }; +export type ValidConfigType = Exclude< + Protobuf.Config.Config["payloadVariant"]["case"], + "deviceUi" | "sessionkey" | undefined +>; +export type ValidModuleConfigType = Exclude< + Protobuf.ModuleConfig.ModuleConfig["payloadVariant"]["case"], + undefined +>; export interface Device { id: number; @@ -54,6 +62,7 @@ export interface Device { unsafeRoles: boolean; refreshKeys: boolean; deleteMessages: boolean; + managedMode: boolean; }; setStatus: (status: Types.DeviceStatusEnum) => void; @@ -61,6 +70,26 @@ export interface Device { setModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void; setWorkingConfig: (config: Protobuf.Config.Config) => void; setWorkingModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => void; + getWorkingConfig: ( + payloadVariant: ValidConfigType, + ) => + | Protobuf.LocalOnly.LocalConfig[Exclude] + | undefined; + getWorkingModuleConfig: ( + payloadVariant: ValidModuleConfigType, + ) => + | Protobuf.LocalOnly.LocalModuleConfig[ + Exclude + ] + | undefined; + removeWorkingConfig: (payloadVariant?: ValidConfigType) => void; + removeWorkingModuleConfig: (payloadVariant?: ValidModuleConfigType) => void; + getEffectiveConfig( + payloadVariant: K, + ): Protobuf.LocalOnly.LocalConfig[K] | undefined; + getEffectiveModuleConfig( + payloadVariant: K, + ): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined; setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => void; setActiveNode: (node: number) => void; setPendingSettingsChanges: (state: boolean) => void; @@ -85,6 +114,8 @@ export interface Device { hasNodeError: (nodeNum: number) => boolean; incrementUnread: (nodeNum: number) => void; resetUnread: (nodeNum: number) => void; + getUnreadCount: (nodeNum: number) => number; + getAllUnreadCount: () => number; getNodes: ( filter?: (node: Protobuf.Mesh.NodeInfo) => boolean, ) => Protobuf.Mesh.NodeInfo[]; @@ -114,7 +145,7 @@ export const useDeviceStore = createStore((set, get) => ({ addDevice: (id: number) => { set( - produce((draft) => { + produce((draft) => { draft.devices.set(id, { id, status: Types.DeviceStatusEnum.DeviceDisconnected, @@ -142,6 +173,7 @@ export const useDeviceStore = createStore((set, get) => ({ refreshKeys: false, rebootOTA: false, deleteMessages: false, + managedMode: false, }, pendingSettingsChanges: false, messageDraft: "", @@ -151,7 +183,7 @@ export const useDeviceStore = createStore((set, get) => ({ setStatus: (status: Types.DeviceStatusEnum) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.status = status; @@ -161,7 +193,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, setConfig: (config: Protobuf.Config.Config) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { switch (config.payloadVariant.case) { @@ -203,7 +235,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, setModuleConfig: (config: Protobuf.ModuleConfig.ModuleConfig) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { switch (config.payloadVariant.case) { @@ -271,12 +303,13 @@ export const useDeviceStore = createStore((set, get) => ({ }, setWorkingConfig: (config: Protobuf.Config.Config) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) return; const index = device.workingConfig.findIndex( (wc) => wc.payloadVariant.case === config.payloadVariant.case, ); + if (index !== -1) { device.workingConfig[index] = config; } else { @@ -289,7 +322,7 @@ export const useDeviceStore = createStore((set, get) => ({ moduleConfig: Protobuf.ModuleConfig.ModuleConfig, ) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) return; const index = device.workingModuleConfig.findIndex( @@ -297,6 +330,7 @@ export const useDeviceStore = createStore((set, get) => ({ wmc.payloadVariant.case === moduleConfig.payloadVariant.case, ); + if (index !== -1) { device.workingModuleConfig[index] = moduleConfig; } else { @@ -305,9 +339,109 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, + + getWorkingConfig: (payloadVariant: ValidConfigType) => { + const device = get().devices.get(id); + if (!device) return; + + const workingConfig = device.workingConfig.find( + (c) => c.payloadVariant.case === payloadVariant, + ); + + if ( + workingConfig?.payloadVariant.case === "deviceUi" || + workingConfig?.payloadVariant.case === "sessionkey" + ) return; + + return workingConfig?.payloadVariant.value; + }, + getWorkingModuleConfig: (payloadVariant: ValidModuleConfigType) => { + const device = get().devices.get(id); + if (!device) return; + + return device.workingModuleConfig.find( + (c) => c.payloadVariant.case === payloadVariant, + )?.payloadVariant.value; + }, + + removeWorkingConfig: (payloadVariant?: ValidConfigType) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) return; + + if (!payloadVariant) { + device.workingConfig = []; + return; + } + + const index = device.workingConfig.findIndex( + (wc: Protobuf.Config.Config) => + wc.payloadVariant.case === payloadVariant, + ); + + if (index !== -1) { + device.workingConfig.splice(index, 1); + } + }), + ); + }, + removeWorkingModuleConfig: ( + payloadVariant?: ValidModuleConfigType, + ) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) return; + + if (!payloadVariant) { + device.workingModuleConfig = []; + return; + } + + const index = device.workingModuleConfig.findIndex( + (wc: Protobuf.ModuleConfig.ModuleConfig) => + wc.payloadVariant.case === payloadVariant, + ); + + if (index !== -1) { + device.workingModuleConfig.splice(index, 1); + } + }), + ); + }, + + getEffectiveConfig( + payloadVariant: K, + ): Protobuf.LocalOnly.LocalConfig[K] | undefined { + if (!payloadVariant) return; + const device = get().devices.get(id); + if (!device) return; + + return { + ...device.config[payloadVariant], + ...(device.workingConfig.find( + (c) => c.payloadVariant.case === payloadVariant, + )?.payloadVariant.value), + }; + }, + getEffectiveModuleConfig( + payloadVariant: K, + ): Protobuf.LocalOnly.LocalModuleConfig[K] | undefined { + const device = get().devices.get(id); + if (!device) return; + + return { + ...device.moduleConfig[payloadVariant], + ...(device.workingModuleConfig.find( + (c) => c.payloadVariant.case === payloadVariant, + )?.payloadVariant.value), + }; + }, + setHardware: (hardware: Protobuf.Mesh.MyNodeInfo) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.hardware = hardware; @@ -317,7 +451,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, setPendingSettingsChanges: (state) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.pendingSettingsChanges = state; @@ -327,7 +461,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addChannel: (channel: Protobuf.Channel.Channel) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.channels.set(channel.index, channel); @@ -337,7 +471,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addWaypoint: (waypoint: Protobuf.Mesh.Waypoint) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { const index = device.waypoints.findIndex((wp) => @@ -354,7 +488,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addNodeInfo: (nodeInfo) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) return; @@ -364,7 +498,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, setActiveNode: (node) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.activeNode = node; @@ -374,7 +508,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addUser: (user) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) { return; @@ -389,7 +523,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addPosition: (position) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) { return; @@ -404,7 +538,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addConnection: (connection) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.connection = connection; @@ -414,7 +548,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addMetadata: (from, metadata) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.metadata.set(from, metadata); @@ -424,7 +558,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, addTraceRoute: (traceroute) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) return; const routes = device.traceroutes.get(traceroute.from) ?? []; @@ -433,9 +567,9 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - removeNode: (nodeNum) => { + removeNode: (nodeNum: number) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) { return; @@ -446,7 +580,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, setDialogOpen: (dialog: DialogVariant, open: boolean) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.dialog[dialog] = open; @@ -461,7 +595,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, processPacket(data: ProcessPacketParams) { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) return; const node = device.nodesMap.get(data.from); @@ -484,7 +618,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, setMessageDraft: (message: string) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.messageDraft = message; @@ -492,9 +626,9 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, - setNodeError: (nodeNum, error) => { + setNodeError: (nodeNum: number, error: string) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.nodeErrors.set(nodeNum, { node: nodeNum, error }); @@ -504,7 +638,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, clearNodeError: (nodeNum: number) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (device) { device.nodeErrors.delete(nodeNum); @@ -524,7 +658,7 @@ export const useDeviceStore = createStore((set, get) => ({ }, incrementUnread: (nodeNum: number) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) return; const currentCount = device.unreadCounts.get(nodeNum) ?? 0; @@ -532,9 +666,27 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, + getUnreadCount: (nodeNum: number): number => { + const device = get().devices.get(id); + if (!device) { + return 0; + } + return device.unreadCounts.get(nodeNum) ?? 0; + }, + getAllUnreadCount: (): number => { + const device = get().devices.get(id); + if (!device) { + return 0; + } + let totalUnread = 0; + device.unreadCounts.forEach((count) => { + totalUnread += count; + }); + return totalUnread; + }, resetUnread: (nodeNum: number) => { set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); if (!device) return; device.unreadCounts.set(nodeNum, 0); @@ -610,10 +762,12 @@ export const useDeviceStore = createStore((set, get) => ({ })); set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); const node = device?.nodesMap.get(nodeNum); - node.isFavorite = isFavorite; + if (node) { + node.isFavorite = isFavorite; + } }), ); }, @@ -631,10 +785,12 @@ export const useDeviceStore = createStore((set, get) => ({ })); set( - produce((draft) => { + produce((draft) => { const device = draft.devices.get(id); const node = device?.nodesMap.get(nodeNum); - node.isIgnored = isIgnored; + if (node) { + node.isIgnored = isIgnored; + } }), ); }, @@ -651,7 +807,7 @@ export const useDeviceStore = createStore((set, get) => ({ removeDevice: (id) => { set( - produce((draft) => { + produce((draft) => { draft.devices.delete(id); }), ); diff --git a/src/core/stores/messageStore/index.ts b/packages/web/src/core/stores/messageStore/index.ts similarity index 100% rename from src/core/stores/messageStore/index.ts rename to packages/web/src/core/stores/messageStore/index.ts diff --git a/src/core/stores/messageStore/messageStore.test.ts b/packages/web/src/core/stores/messageStore/messageStore.test.ts similarity index 100% rename from src/core/stores/messageStore/messageStore.test.ts rename to packages/web/src/core/stores/messageStore/messageStore.test.ts diff --git a/src/core/stores/messageStore/types.ts b/packages/web/src/core/stores/messageStore/types.ts similarity index 100% rename from src/core/stores/messageStore/types.ts rename to packages/web/src/core/stores/messageStore/types.ts diff --git a/src/core/stores/sidebarStore.tsx b/packages/web/src/core/stores/sidebarStore.tsx similarity index 100% rename from src/core/stores/sidebarStore.tsx rename to packages/web/src/core/stores/sidebarStore.tsx diff --git a/src/core/stores/storage/indexDB.ts b/packages/web/src/core/stores/storage/indexDB.ts similarity index 86% rename from src/core/stores/storage/indexDB.ts rename to packages/web/src/core/stores/storage/indexDB.ts index d7d89342..ce2c2790 100644 --- a/src/core/stores/storage/indexDB.ts +++ b/packages/web/src/core/stores/storage/indexDB.ts @@ -1,4 +1,4 @@ -import { PersistStorage, StateStorage } from "zustand/middleware"; +import { PersistStorage, StateStorage, StorageValue } from "zustand/middleware"; import { del, get, set } from "idb-keyval"; import { ChannelId, MessageLogMap } from "@core/stores/messageStore/types.ts"; @@ -56,18 +56,25 @@ const reviver: JsonReviver = (_, value) => { }; export const storageWithMapSupport: PersistStorage = { - getItem: async (name): Promise => { + getItem: async ( + name, + ): Promise | null> => { const str = await zustandIndexDBStorage.getItem(name); if (!str) return null; try { - const parsed = JSON.parse(str, reviver) as PersistedMessageState; + const parsed = JSON.parse(str, reviver) as StorageValue< + PersistedMessageState + >; return parsed; } catch (error) { console.error(`Error parsing persisted state (${name}):`, error); return null; } }, - setItem: async (name, newValue: PersistedMessageState): Promise => { + setItem: async ( + name, + newValue: StorageValue, + ): Promise => { try { const str = JSON.stringify(newValue, replacer); await zustandIndexDBStorage.setItem(name, str); diff --git a/src/core/subscriptions.ts b/packages/web/src/core/subscriptions.ts similarity index 100% rename from src/core/subscriptions.ts rename to packages/web/src/core/subscriptions.ts diff --git a/src/core/utils/bitwise.ts b/packages/web/src/core/utils/bitwise.ts similarity index 100% rename from src/core/utils/bitwise.ts rename to packages/web/src/core/utils/bitwise.ts diff --git a/src/core/utils/cn.ts b/packages/web/src/core/utils/cn.ts similarity index 100% rename from src/core/utils/cn.ts rename to packages/web/src/core/utils/cn.ts diff --git a/src/core/utils/debounce.test.ts b/packages/web/src/core/utils/debounce.test.ts similarity index 100% rename from src/core/utils/debounce.test.ts rename to packages/web/src/core/utils/debounce.test.ts diff --git a/src/core/utils/debounce.ts b/packages/web/src/core/utils/debounce.ts similarity index 100% rename from src/core/utils/debounce.ts rename to packages/web/src/core/utils/debounce.ts diff --git a/packages/web/src/core/utils/deepCompareConfig.test.ts b/packages/web/src/core/utils/deepCompareConfig.test.ts new file mode 100644 index 00000000..b504d2b7 --- /dev/null +++ b/packages/web/src/core/utils/deepCompareConfig.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { deepCompareConfig } from "./deepCompareConfig.ts"; + +describe("deepCompareConfig", () => { + it("returns true for identical primitives", () => { + expect(deepCompareConfig(5, 5)).toBe(true); + expect(deepCompareConfig("foo", "foo")).toBe(true); + expect(deepCompareConfig(true, true)).toBe(true); + }); + + it("returns false for different primitives", () => { + expect(deepCompareConfig(5, 6)).toBe(false); + expect(deepCompareConfig("foo", "bar")).toBe(false); + expect(deepCompareConfig(true, false)).toBe(false); + }); + + it("handles nulls correctly", () => { + expect(deepCompareConfig(null, null)).toBe(true); + expect(deepCompareConfig(null, undefined)).toBe(false); + expect(deepCompareConfig(null, {})).toBe(false); + }); + + it("allows undefined in working when allowUndefined is true", () => { + expect(deepCompareConfig({ a: 1 }, { a: undefined }, true)).toBe(true); + expect(deepCompareConfig([1, 2, 3], [1, undefined, 3], true)).toBe(true); + }); + + it("rejects undefined in working when allowUndefined is false", () => { + expect(deepCompareConfig({ a: 1 }, { a: undefined }, false)).toBe(false); + }); + + it("compares arrays deeply", () => { + expect(deepCompareConfig([1, [2, 3]], [1, [2, 3]])).toBe(true); + expect(deepCompareConfig([1, [2, 3]], [1, [2, 4]])).toBe(false); + }); + + it("compares objects deeply", () => { + const existing = { x: 10, y: { z: 20 } }; + const workingEqual = { x: 10, y: { z: 20 } }; + const workingDiff = { x: 10, y: { z: 21 } }; + + expect(deepCompareConfig(existing, workingEqual)).toBe(true); + expect(deepCompareConfig(existing, workingDiff)).toBe(false); + }); + + it("ignores $typeName key in existing", () => { + const existing = { $typeName: "Test", a: 1 }; + const working = { a: 1 }; + expect(deepCompareConfig(existing, working)).toBe(true); + }); + + it("fails when working has extra keys", () => { + expect(deepCompareConfig({ a: 1 }, { a: 1, b: 2 })).toBe(false); + }); + + it("allows working arrays to be shorter if allowUndefined is true", () => { + expect(deepCompareConfig([1, 2, 3, 4], [1, 2], true)).toBe(true); + expect(deepCompareConfig([1, 2, 3, 4], [1, 2], false)).toBe(false); + }); +}); diff --git a/packages/web/src/core/utils/deepCompareConfig.ts b/packages/web/src/core/utils/deepCompareConfig.ts new file mode 100644 index 00000000..aca12452 --- /dev/null +++ b/packages/web/src/core/utils/deepCompareConfig.ts @@ -0,0 +1,58 @@ +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function deepCompareConfig( + a: unknown, + b: unknown, + allowUndefined = false, +): boolean { + if (a === b) { + return true; + } + + // If allowUndefined is true, and one is undefined, they are considered equal. // This check is placed early to simplify subsequent logic. + if (allowUndefined && (a === undefined || b === undefined)) { + return true; + } + + if (typeof a !== typeof b || a === null || b === null) { + return false; + } + + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length && !allowUndefined) { + return false; + } + + const longestLength = Math.max(a.length, b.length); + for (let i = 0; i < longestLength; i++) { + if (!deepCompareConfig(a[i], b[i], allowUndefined)) { + return false; + } + } + return true; + } + + if (isObject(a) && isObject(b)) { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + const allKeys = new Set([...aKeys, ...bKeys]); + + for (const key of allKeys) { + if (key === "$typeName") { + continue; + } + + const aValue = a[key]; + const bValue = b[key]; + + if (!deepCompareConfig(aValue, bValue, allowUndefined)) { + return false; + } + } + return true; + } + + return false; +} diff --git a/src/core/utils/dotPath.test.ts b/packages/web/src/core/utils/dotPath.test.ts similarity index 100% rename from src/core/utils/dotPath.test.ts rename to packages/web/src/core/utils/dotPath.test.ts diff --git a/src/core/utils/dotPath.ts b/packages/web/src/core/utils/dotPath.ts similarity index 100% rename from src/core/utils/dotPath.ts rename to packages/web/src/core/utils/dotPath.ts diff --git a/src/core/utils/eventBus.test.ts b/packages/web/src/core/utils/eventBus.test.ts similarity index 98% rename from src/core/utils/eventBus.test.ts rename to packages/web/src/core/utils/eventBus.test.ts index 6d2fc6ee..2d874066 100644 --- a/src/core/utils/eventBus.test.ts +++ b/packages/web/src/core/utils/eventBus.test.ts @@ -4,7 +4,7 @@ import { eventBus } from "@core/utils/eventBus.ts"; describe("EventBus", () => { beforeEach(() => { // Reset event listeners before each test - eventBus.listeners = {}; + eventBus.offAll(); }); it("should register an event listener and trigger it on emit", () => { diff --git a/src/core/utils/eventBus.ts b/packages/web/src/core/utils/eventBus.ts similarity index 87% rename from src/core/utils/eventBus.ts rename to packages/web/src/core/utils/eventBus.ts index 618ab5a6..75c48d8c 100644 --- a/src/core/utils/eventBus.ts +++ b/packages/web/src/core/utils/eventBus.ts @@ -34,6 +34,14 @@ class EventBus { } } + public offAll(event?: T): void { + if (event) { + this.listeners[event] = []; + } else { + this.listeners = {}; + } + } + public emit(event: T, data: EventMap[T]): void { if (!this.listeners[event]) return; diff --git a/src/core/utils/github.ts b/packages/web/src/core/utils/github.ts similarity index 100% rename from src/core/utils/github.ts rename to packages/web/src/core/utils/github.ts diff --git a/src/core/utils/hostname.ts b/packages/web/src/core/utils/hostname.ts similarity index 100% rename from src/core/utils/hostname.ts rename to packages/web/src/core/utils/hostname.ts diff --git a/src/core/utils/ip.test.ts b/packages/web/src/core/utils/ip.test.ts similarity index 97% rename from src/core/utils/ip.test.ts rename to packages/web/src/core/utils/ip.test.ts index 2da62e57..6208e6d3 100644 --- a/src/core/utils/ip.test.ts +++ b/packages/web/src/core/utils/ip.test.ts @@ -60,7 +60,7 @@ describe("IP Address Conversion Functions", () => { for (const ip of testIps) { const int = convertIpAddressToInt(ip); expect(int).not.toBeNull(); - if (int !== null) { + if (int !== null && typeof int === "number") { const convertedBack = convertIntToIpAddress(int); expect(convertedBack).toBe(ip); } diff --git a/src/core/utils/ip.ts b/packages/web/src/core/utils/ip.ts similarity index 100% rename from src/core/utils/ip.ts rename to packages/web/src/core/utils/ip.ts diff --git a/src/core/utils/randId.test.ts b/packages/web/src/core/utils/randId.test.ts similarity index 100% rename from src/core/utils/randId.test.ts rename to packages/web/src/core/utils/randId.test.ts diff --git a/src/core/utils/randId.ts b/packages/web/src/core/utils/randId.ts similarity index 100% rename from src/core/utils/randId.ts rename to packages/web/src/core/utils/randId.ts diff --git a/packages/web/src/core/utils/sort.ts b/packages/web/src/core/utils/sort.ts new file mode 100644 index 00000000..2d1494f0 --- /dev/null +++ b/packages/web/src/core/utils/sort.ts @@ -0,0 +1,18 @@ +export function intlSort( + arr: T[], + order: "asc" | "desc" = "asc", + locale: Intl.Locale, +): T[] { + const collator = new Intl.Collator(locale, { sensitivity: "base" }); + + return arr.sort((a, b) => { + const stringA = String(a); + const stringB = String(b); + + if (order === "asc") { + return collator.compare(stringA, stringB); + } else { + return collator.compare(stringB, stringA); + } + }); +} diff --git a/src/core/utils/string.ts b/packages/web/src/core/utils/string.ts similarity index 100% rename from src/core/utils/string.ts rename to packages/web/src/core/utils/string.ts diff --git a/src/core/utils/x25519.ts b/packages/web/src/core/utils/x25519.ts similarity index 100% rename from src/core/utils/x25519.ts rename to packages/web/src/core/utils/x25519.ts diff --git a/packages/web/src/i18n-config.ts b/packages/web/src/i18n-config.ts new file mode 100644 index 00000000..a9663b25 --- /dev/null +++ b/packages/web/src/i18n-config.ts @@ -0,0 +1,62 @@ +import i18next from "i18next"; +import { initReactI18next } from "react-i18next"; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; + +export type Lang = { + code: Intl.Locale["language"]; + name: string; + flag: string; + region?: Intl.Locale["region"]; +}; + +export type LangCode = Lang["code"]; + +export const supportedLanguages: Lang[] = [ + { code: "de", name: "Deutsch", flag: "🇩🇪" }, + { code: "en", name: "English", flag: "🇺🇸" }, + { code: "fi", name: "Suomi", flag: "🇫🇮" }, + { code: "sv", name: "Svenska", flag: "🇸🇪" }, +]; + +export const FALLBACK_LANGUAGE_CODE: LangCode = "en"; + +i18next + .use(Backend) + .use(initReactI18next) + .use(LanguageDetector) + .init({ + backend: { + // With this setup, {{lng}} will correctly resolve to 'en-US', 'fi-FI', etc. + loadPath: "/i18n/locales/{{lng}}/{{ns}}.json", + }, + react: { + useSuspense: true, + }, + nonExplicitSupportedLngs: true, + detection: { + order: ["localStorage", "navigator"], + caches: ["localStorage"], + }, + fallbackLng: { + default: [FALLBACK_LANGUAGE_CODE], + "en-GB": [FALLBACK_LANGUAGE_CODE], + "fi": ["fi-FI", FALLBACK_LANGUAGE_CODE], + "sv": ["sv-SE", FALLBACK_LANGUAGE_CODE], + "de": ["de-DE", FALLBACK_LANGUAGE_CODE], + }, + fallbackNS: ["common", "ui", "dialog"], + debug: import.meta.env.MODE === "development", + ns: [ + "channels", + "commandPalette", + "common", + "deviceConfig", + "moduleConfig", + "dashboard", + "dialog", + "messages", + "nodes", + "ui", + ], + }); diff --git a/src/index.css b/packages/web/src/index.css similarity index 98% rename from src/index.css rename to packages/web/src/index.css index 1ae593b4..56103edb 100644 --- a/src/index.css +++ b/packages/web/src/index.css @@ -3,6 +3,10 @@ @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); +@view-transition { + navigation: auto; +} + @theme { --font-mono: Cascadia Code, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx new file mode 100644 index 00000000..d66afd57 --- /dev/null +++ b/packages/web/src/index.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import "@app/index.css"; +import { enableMapSet } from "immer"; +import "maplibre-gl/dist/maplibre-gl.css"; +import { Suspense } from "react"; +import { createRoot } from "react-dom/client"; +import "./i18n-config.ts"; +import { createRouter, RouterProvider } from "@tanstack/react-router"; +import { router } from "@app/routes.tsx"; +import { useAppStore } from "@core/stores/appStore.ts"; +import { useMessageStore } from "@core/stores/messageStore/index.ts"; +import { useTranslation } from "react-i18next"; + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} +const container = document.getElementById("root") as HTMLElement; +const root = createRoot(container); + +function IndexPage() { + enableMapSet(); + const appStore = useAppStore(); + const messageStore = useMessageStore(); + const translation = useTranslation(); + + const context = React.useMemo(() => ({ + stores: { + app: appStore, + message: messageStore, + }, + i18n: translation, + }), [appStore, messageStore]); + + return ( + + + + + + ); +} + +root.render(); diff --git a/src/pages/Channels.tsx b/packages/web/src/pages/Channels.tsx similarity index 100% rename from src/pages/Channels.tsx rename to packages/web/src/pages/Channels.tsx diff --git a/packages/web/src/pages/Config/DeviceConfig.tsx b/packages/web/src/pages/Config/DeviceConfig.tsx new file mode 100644 index 00000000..314d5f26 --- /dev/null +++ b/packages/web/src/pages/Config/DeviceConfig.tsx @@ -0,0 +1,119 @@ +import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx"; +import { Device } from "@components/PageComponents/Config/Device/index.tsx"; +import { Display } from "@components/PageComponents/Config/Display.tsx"; +import { LoRa } from "@components/PageComponents/Config/LoRa.tsx"; +import { Network } from "@components/PageComponents/Config/Network/index.tsx"; +import { Position } from "@components/PageComponents/Config/Position.tsx"; +import { Power } from "@components/PageComponents/Config/Power.tsx"; +import { Security } from "@components/PageComponents/Config/Security/Security.tsx"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@components/UI/Tabs.tsx"; +import { Spinner } from "@components/UI/Spinner.tsx"; +import { useTranslation } from "react-i18next"; +import { useDevice, type ValidConfigType } from "@core/stores/deviceStore.ts"; +import { useMemo } from "react"; +import { type ComponentType, Suspense } from "react"; +import type { UseFormReturn } from "react-hook-form"; +import { ConfigSuspender } from "@components/PageComponents/Config/ConfigSuspender.tsx"; + +interface ConfigProps { + // We can get rid of this exception if we import every config schema and pass the union type + // deno-lint-ignore no-explicit-any + onFormInit: (methods: UseFormReturn) => void; +} +type TabItem = { + case: ValidConfigType; + label: string; + element: ComponentType; + count?: number; +}; + +export const DeviceConfig = ({ onFormInit }: ConfigProps) => { + const { getWorkingConfig } = useDevice(); + const { t } = useTranslation("deviceConfig"); + const tabs: TabItem[] = [ + { + case: "device", + label: t("page.tabDevice"), + element: Device, + count: 0, + }, + { + case: "position", + label: t("page.tabPosition"), + element: Position, + }, + { + case: "power", + label: t("page.tabPower"), + element: Power, + }, + { + case: "network", + label: t("page.tabNetwork"), + element: Network, + }, + { + case: "display", + label: t("page.tabDisplay"), + element: Display, + }, + { + case: "lora", + label: t("page.tabLora"), + element: LoRa, + }, + { + case: "bluetooth", + label: t("page.tabBluetooth"), + element: Bluetooth, + }, + { + case: "security", + label: t("page.tabSecurity"), + element: Security, + }, + ] as const; + + const flags = useMemo( + () => new Map(tabs.map((tab) => [tab.case, getWorkingConfig(tab.case)])), + [tabs, getWorkingConfig], + ); + + return ( + + + {tabs.map((tab) => ( + + {tab.label} + {flags.get(tab.case) && ( + + + + + + + )} + + ))} + + {tabs.map((tab) => ( + + }> + + + + + + ))} + + ); +}; diff --git a/src/pages/Config/ModuleConfig.tsx b/packages/web/src/pages/Config/ModuleConfig.tsx similarity index 54% rename from src/pages/Config/ModuleConfig.tsx rename to packages/web/src/pages/Config/ModuleConfig.tsx index 4800cf2b..db0cb106 100644 --- a/src/pages/Config/ModuleConfig.tsx +++ b/packages/web/src/pages/Config/ModuleConfig.tsx @@ -16,60 +16,96 @@ import { TabsList, TabsTrigger, } from "@components/UI/Tabs.tsx"; +import { Spinner } from "@components/UI/Spinner.tsx"; import { useTranslation } from "react-i18next"; +import { + useDevice, + type ValidModuleConfigType, +} from "@core/stores/deviceStore.ts"; +import { useMemo } from "react"; +import { type ComponentType, Suspense } from "react"; +import type { UseFormReturn } from "react-hook-form"; +import { ConfigSuspender } from "@components/PageComponents/Config/ConfigSuspender.tsx"; + +interface ConfigProps { + // We can get rid of this exception if we import every config schema and pass the union type + // deno-lint-ignore no-explicit-any + onFormInit: (methods: UseFormReturn) => void; +} +type TabItem = { + case: ValidModuleConfigType; + label: string; + element: ComponentType; + count?: number; +}; -export const ModuleConfig = () => { +export const ModuleConfig = ({ onFormInit }: ConfigProps) => { + const { getWorkingModuleConfig } = useDevice(); const { t } = useTranslation("moduleConfig"); - const tabs = [ + const tabs: TabItem[] = [ { + case: "mqtt", label: t("page.tabMqtt"), element: MQTT, }, { + case: "serial", label: t("page.tabSerial"), element: Serial, }, { + case: "externalNotification", label: t("page.tabExternalNotification"), element: ExternalNotification, }, { + case: "storeForward", label: t("page.tabStoreAndForward"), element: StoreForward, }, { + case: "rangeTest", label: t("page.tabRangeTest"), element: RangeTest, }, { + case: "telemetry", label: t("page.tabTelemetry"), element: Telemetry, }, { + case: "cannedMessage", label: t("page.tabCannedMessage"), element: CannedMessage, }, { + case: "audio", label: t("page.tabAudio"), element: Audio, }, { + case: "neighborInfo", label: t("page.tabNeighborInfo"), element: NeighborInfo, }, { + case: "ambientLighting", label: t("page.tabAmbientLighting"), element: AmbientLighting, }, { + case: "detectionSensor", label: t("page.tabDetectionSensor"), element: DetectionSensor, }, - { - label: t("page.tabPaxcounter"), - element: Paxcounter, - }, - ]; + { case: "paxcounter", label: t("page.tabPaxcounter"), element: Paxcounter }, + ] as const; + + const flags = useMemo( + () => + new Map(tabs.map((tab) => [tab.case, getWorkingModuleConfig(tab.case)])), + [tabs, getWorkingModuleConfig], + ); return ( @@ -78,15 +114,27 @@ export const ModuleConfig = () => { {tab.label} + {flags.get(tab.case) && ( + + + + + + + )} ))} {tabs.map((tab) => ( - + }> + + + + ))} diff --git a/packages/web/src/pages/Config/index.tsx b/packages/web/src/pages/Config/index.tsx new file mode 100644 index 00000000..224dfdb7 --- /dev/null +++ b/packages/web/src/pages/Config/index.tsx @@ -0,0 +1,237 @@ +import { useAppStore } from "@core/stores/appStore.ts"; +import { useDevice } from "@core/stores/deviceStore.ts"; +import { PageLayout } from "@components/PageLayout.tsx"; +import { Sidebar } from "@components/Sidebar.tsx"; +import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; +import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx"; + +import { useToast } from "@core/hooks/useToast.ts"; +import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx"; +import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx"; +import { + BoxesIcon, + RefreshCwIcon, + SaveIcon, + SaveOff, + SettingsIcon, +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { cn } from "@core/utils/cn.ts"; +import type { UseFormReturn } from "react-hook-form"; + +const ConfigPage = () => { + const { + workingConfig, + workingModuleConfig, + connection, + removeWorkingConfig, + removeWorkingModuleConfig, + setConfig, + setModuleConfig, + } = useDevice(); + const { hasErrors } = useAppStore(); + + const [activeConfigSection, setActiveConfigSection] = useState< + "device" | "module" + >("device"); + const [isSaving, setIsSaving] = useState(false); + const [formMethods, setFormMethods] = useState(null); + const { toast } = useToast(); + const { t } = useTranslation("deviceConfig"); + + const onFormInit = (methods: UseFormReturn) => { + setFormMethods(methods); + }; + + const handleSave = async () => { + if (hasErrors()) { + return toast({ + title: t("toast.validationError.title"), + description: t("toast.validationError.description"), + }); + } + + setIsSaving(true); + + try { + await Promise.all( + workingConfig.map((newConfig) => + connection?.setConfig(newConfig).then(() => { + toast({ + title: t("toast.saveSuccess.title"), + description: t("toast.saveSuccess.description", { + case: newConfig.payloadVariant.case, + }), + }); + }) + ), + ); + + await Promise.all( + workingModuleConfig.map((newModuleConfig) => + connection?.setModuleConfig(newModuleConfig).then(() => + toast({ + title: t("toast.saveSuccess.title"), + description: t("toast.saveSuccess.description", { + case: newModuleConfig.payloadVariant.case, + }), + }) + ) + ), + ); + + await connection?.commitEditSettings().then(() => { + if (formMethods) { + formMethods.reset({}, { + keepValues: true, + }); + } + + workingConfig.map((newConfig) => setConfig(newConfig)); + workingModuleConfig.map((newModuleConfig) => + setModuleConfig(newModuleConfig) + ); + + removeWorkingConfig(); + removeWorkingModuleConfig(); + }); + } catch (_error) { + toast({ + title: t("toast.configSaveError.title"), + description: t("toast.configSaveError.description"), + }); + } finally { + setIsSaving(false); + } + }; + + const handleReset = () => { + if (formMethods) { + formMethods.reset(); + } + + removeWorkingConfig(); + removeWorkingModuleConfig(); + }; + + const leftSidebar = useMemo( + () => ( + + + setActiveConfigSection("device")} + Icon={SettingsIcon} + isDirty={workingConfig.length > 0} + count={workingConfig.length} + /> + setActiveConfigSection("module")} + Icon={BoxesIcon} + isDirty={workingModuleConfig.length > 0} + count={workingModuleConfig.length} + /> + + + ), + [activeConfigSection, workingConfig, workingModuleConfig], + ); + + const buttonOpacity = useMemo(() => { + const isFormDirty = formMethods?.formState.isDirty ?? false; + const hasDirtyFields = + (Object.keys(formMethods?.formState.dirtyFields ?? {}).length ?? 0) > 0; + const hasWorkingConfig = workingConfig.length > 0; + const hasWorkingModuleConfig = workingModuleConfig.length > 0; + + const shouldShowButton = (isFormDirty && hasDirtyFields) || + hasWorkingConfig || hasWorkingModuleConfig; + + return shouldShowButton ? "opacity-100" : "opacity-0"; + }, [ + formMethods?.formState.isDirty, + formMethods?.formState.dirtyFields, + workingConfig, + workingModuleConfig, + ]); + + const isValid = useMemo(() => { + return Object.keys(formMethods?.formState.errors ?? {}).length === 0; + }, [formMethods?.formState.errors]); + + const actions = useMemo(() => [ + { + key: "unsavedChanges", + label: t("common:formValidation.unsavedChanges"), + onClick: () => {}, + className: cn([ + "bg-blue-500 text-slate-900 hover:bg-initial", + "transition-colors duration-200", + buttonOpacity, + "transition-opacity", + ]), + }, + { + key: "reset", + icon: RefreshCwIcon, + label: t("common:button.reset"), + onClick: handleReset, + className: cn([ + buttonOpacity, + "transition-opacity hover:bg-slate-200 disabled:hover:bg-white", + "hover:dark:bg-slate-300 hover:dark:text-black cursor-pointer", + ]), + }, + { + key: "save", + icon: !isValid ? SaveOff : SaveIcon, + isLoading: isSaving, + disabled: isSaving || + !isValid || + (workingConfig.length === 0 && workingModuleConfig.length === 0), + iconClasses: !isValid + ? "text-red-400 cursor-not-allowed" + : "cursor-pointer", + className: cn([ + "transition-opacity hover:bg-slate-200 disabled:hover:bg-white", + "hover:dark:bg-slate-300 hover:dark:text-black", + "disabled:hover:cursor-not-allowed cursor-pointer", + ]), + onClick: handleSave, + label: t("common:button.save"), + }, + ], [ + activeConfigSection, + isSaving, + isValid, + buttonOpacity, + workingConfig, + workingModuleConfig, + ]); + + return ( + <> + + {activeConfigSection === "device" + ? + : } + + + ); +}; + +export default ConfigPage; diff --git a/src/pages/Dashboard/index.tsx b/packages/web/src/pages/Dashboard/index.tsx similarity index 74% rename from src/pages/Dashboard/index.tsx rename to packages/web/src/pages/Dashboard/index.tsx index 17e87e11..03cfc5d7 100644 --- a/src/pages/Dashboard/index.tsx +++ b/packages/web/src/pages/Dashboard/index.tsx @@ -6,13 +6,7 @@ import { Separator } from "@components/UI/Seperator.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { NewConnectionDialog } from "@components/Dialog/NewConnectionDialog.tsx"; import { ConnectionTabs } from "@components/ConnectionTabs/ConnectionTabs.tsx"; -import { - BluetoothIcon, - NetworkIcon, - PlusIcon, - UsbIcon, - UsersIcon, -} from "lucide-react"; +import { PlusIcon, UsersIcon } from "lucide-react"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import LanguageSwitcher from "@components/LanguageSwitcher.tsx"; @@ -27,7 +21,6 @@ export const Dashboard = () => { // Show connection dialog only if user explicitly opens it // When no devices exist, we show inline connection tabs instead - return ( <> { ?.longName ?? t("unknown.shortName")}

-
- {device.connection?.connType === "ble" && ( - <> - - {t( - "dashboard.connectionType_ble", - )} - - )} - {device.connection?.connType === "serial" && ( - <> - - {t( - "dashboard.connectionType_serial", - )} - - )} - {device.connection?.connType === "http" && ( - <> - - {t( - "dashboard.connectionType_network", - )} - - )} -
+ {t("selectChatPrompt.text", { ns: "messages" })} +
+ ); +} + export const MessagesPage = () => { const { channels, getNodes, getNode, hasNodeError, - unreadCounts, + getUnreadCount, resetUnread, connection, } = useDevice(); @@ -46,7 +56,9 @@ export const MessagesPage = () => { getMessages, setMessageState, } = useMessageStore(); - const params = useParams({ from: "", shouldThrow: false }); + + const { type, chatId } = useParams({ from: messagesWithParamsRoute.id }); + const navigate = useNavigate(); const { toast } = useToast(); const { isCollapsed } = useSidebar(); @@ -54,34 +66,33 @@ export const MessagesPage = () => { const { t } = useTranslation(["messages", "channels", "ui"]); const deferredSearch = useDeferredValue(searchTerm); - const chatType = params.type === "direct" + const navigateToChat = useCallback((type: MessageType, id: string) => { + const typeParam = type === MessageType.Direct ? "direct" : "broadcast"; + navigate({ to: `/messages/${typeParam}/${id}` }); + }, [navigate]); + + const chatType = type === "direct" ? MessageType.Direct - : params.type === "broadcast" - ? MessageType.Broadcast - : undefined; - const activeChat = params.chatId ? Number(params.chatId) : undefined; + : MessageType.Broadcast; + const numericChatId = Number(chatId); const allChannels = Array.from(channels.values()); const filteredChannels = allChannels.filter( (ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED, ); - const currentChannel = channels.get(activeChat); - const otherNode = getNode(activeChat); - - const isDirect = chatType === MessageType.Direct; - const isBroadcast = chatType === MessageType.Broadcast; - - const navigateToChat = useCallback((type: MessageType, chatId: number) => { - const typeParam = type === MessageType.Direct ? "direct" : "broadcast"; - navigate({ to: `/messages/${typeParam}/${chatId}` }); - }, [navigate]); useEffect(() => { - if (!params.type && !params.chatId && filteredChannels.length > 0) { + if (!type && !chatId && filteredChannels.length > 0) { const defaultChannel = filteredChannels[0]; - navigateToChat(MessageType.Broadcast, defaultChannel.index); + navigateToChat(MessageType.Broadcast, defaultChannel.index.toString()); } - }, [params.type, params.chatId, filteredChannels, navigateToChat]); + }, [type, chatId, filteredChannels, navigateToChat]); + + const currentChannel = channels.get(numericChatId); + const otherNode = getNode(numericChatId); + + const isDirect = chatType === MessageType.Direct; + const isBroadcast = chatType === MessageType.Broadcast; const filteredNodes = (): NodeInfoWithUnread[] => { const lowerCaseSearchTerm = deferredSearch.toLowerCase(); @@ -94,7 +105,7 @@ export const MessagesPage = () => { }) .map((node) => ({ ...node, - unreadCount: unreadCounts.get(node.num) ?? 0, + unreadCount: getUnreadCount(node.num) ?? 0, })) .sort((a, b) => { const diff = b.unreadCount - a.unreadCount; @@ -104,12 +115,8 @@ export const MessagesPage = () => { }; const sendText = useCallback(async (message: string) => { - const isDirect = chatType === MessageType.Direct; - const toValue = isDirect ? activeChat : MessageType.Broadcast; - - const channelValue = isDirect - ? Types.ChannelNumber.Primary - : activeChat ?? 0; + const toValue = isDirect ? numericChatId : MessageType.Broadcast; + const channelValue = isDirect ? Types.ChannelNumber.Primary : numericChatId; let messageId: number | undefined; @@ -123,16 +130,16 @@ export const MessagesPage = () => { if (messageId !== undefined) { if (chatType === MessageType.Broadcast) { setMessageState({ - type: chatType, + type: MessageType.Broadcast, channelId: channelValue, messageId, newState: MessageState.Ack, }); } else { setMessageState({ - type: chatType, + type: MessageType.Direct, nodeA: getMyNodeNum(), - nodeB: activeChat, + nodeB: numericChatId, messageId, newState: MessageState.Ack, }); @@ -145,23 +152,29 @@ export const MessagesPage = () => { const failedId = messageId ?? randId(); if (chatType === MessageType.Broadcast) { setMessageState({ - type: chatType, + type: MessageType.Broadcast, channelId: channelValue, messageId: failedId, newState: MessageState.Failed, }); - } else { // MessageType.Direct - const failedId = messageId ?? randId(); + } else { setMessageState({ - type: chatType, + type: MessageType.Direct, nodeA: getMyNodeNum(), - nodeB: activeChat, + nodeB: numericChatId, messageId: failedId, newState: MessageState.Failed, }); } } - }, [activeChat, chatType, connection, getMyNodeNum, setMessageState]); + }, [ + numericChatId, + chatId, + chatType, + connection, + getMyNodeNum, + setMessageState, + ]); const renderChatContent = () => { switch (chatType) { @@ -170,7 +183,7 @@ export const MessagesPage = () => { ); @@ -180,16 +193,12 @@ export const MessagesPage = () => { messages={getMessages({ type: MessageType.Direct, nodeA: getMyNodeNum(), - nodeB: activeChat, + nodeB: numericChatId, }).reverse()} /> ); default: - return ( -
- {t("selectChatPrompt.text", { ns: "messages" })} -
- ); + return ; } }; @@ -202,7 +211,7 @@ export const MessagesPage = () => { {filteredChannels?.map((channel) => ( { index: channel.index, ns: "channels", }))} - active={activeChat === channel.index && + active={numericChatId === channel.index && chatType === MessageType.Broadcast} onClick={() => { - navigateToChat(MessageType.Broadcast, channel.index); + navigateToChat(MessageType.Broadcast, channel.index.toString()); resetUnread(channel.index); }} > @@ -227,12 +236,13 @@ export const MessagesPage = () => { ), [ filteredChannels, - unreadCounts, - activeChat, + numericChatId, chatType, isCollapsed, + getUnreadCount, navigateToChat, resetUnread, + t, ]); const rightSidebar = useMemo( @@ -262,10 +272,10 @@ export const MessagesPage = () => { label={node.user?.longName ?? t("unknown.shortName")} count={node.unreadCount > 0 ? node.unreadCount : undefined} - active={activeChat === node.num && + active={numericChatId === node.num && chatType === MessageType.Direct} onClick={() => { - navigateToChat(MessageType.Direct, node.num); + navigateToChat(MessageType.Direct, node.num.toString()); resetUnread(node.num); }} > @@ -285,23 +295,27 @@ export const MessagesPage = () => { [ filteredNodes, searchTerm, - activeChat, + numericChatId, chatType, navigateToChat, resetUnread, hasNodeError, + t, ], ); return ( { {(isBroadcast || isDirect) ? ( diff --git a/src/pages/Nodes.tsx b/packages/web/src/pages/Nodes/index.tsx similarity index 53% rename from src/pages/Nodes.tsx rename to packages/web/src/pages/Nodes/index.tsx index c349cde3..fc50ab3f 100644 --- a/src/pages/Nodes.tsx +++ b/packages/web/src/pages/Nodes/index.tsx @@ -3,7 +3,11 @@ import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteRespo import { Sidebar } from "@components/Sidebar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx"; import { Mono } from "@components/generic/Mono.tsx"; -import { Table } from "@components/generic/Table/index.tsx"; +import { + type DataRow, + type Heading, + Table, +} from "@components/generic/Table/index.tsx"; import { TimeAgo } from "@components/generic/TimeAgo.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import { useAppStore } from "@core/stores/appStore.ts"; @@ -27,6 +31,7 @@ import { } from "@components/generic/Filter/useFilterNode.ts"; import { FilterControl } from "@components/generic/Filter/FilterControl.tsx"; import { useTranslation } from "react-i18next"; +import useLang from "@core/hooks/useLang.ts"; export interface DeleteNoteDialogProps { open: boolean; @@ -35,6 +40,7 @@ export interface DeleteNoteDialogProps { const NodesPage = (): JSX.Element => { const { t } = useTranslation("nodes"); + const { currentLanguage } = useLang(); const { getNodes, hardware, connection, hasNodeError, setDialogOpen } = useDevice(); const { setNodeNumDetails } = useAppStore(); @@ -93,6 +99,128 @@ const NodesPage = (): JSX.Element => { setDialogOpen("nodeDetails", true); } + const tableHeadings: Heading[] = [ + { title: "", sortable: false }, + { title: t("nodesTable.headings.longName"), sortable: true }, + { title: t("nodesTable.headings.connection"), sortable: true }, + { title: t("nodesTable.headings.lastHeard"), sortable: true }, + { title: t("nodesTable.headings.encryption"), sortable: false }, + { title: t("unit.snr"), sortable: true }, + { title: t("nodesTable.headings.model"), sortable: true }, + { title: t("nodesTable.headings.macAddress"), sortable: true }, + ]; + + const tableRows: DataRow[] = filteredNodes.map((node) => { + const macAddress = base16 + .stringify(node.user?.macaddr ?? []) + .match(/.{1,2}/g) + ?.join(":") ?? t("unknown.shortName"); + + return { + id: node.num, + isFavorite: node.isFavorite, + cells: [ + { + content: ( + + ), + sortValue: node.user?.shortName ?? "", // Non-sortable column + }, + { + content: ( +

handleNodeInfoDialog(node.num)} + onKeyUp={(evt) => { + evt.key === "Enter" && handleNodeInfoDialog(node.num); + }} + className="cursor-pointer underline ml-2 whitespace-break-spaces" + tabIndex={0} + role="button" + > + {node.user?.longName ?? numberToHexUnpadded(node.num)} +

+ ), + sortValue: node.user?.longName ?? numberToHexUnpadded(node.num), + }, + { + content: ( + + {node.hopsAway !== undefined + ? node?.viaMqtt === false && node.hopsAway === 0 + ? t("nodesTable.connectionStatus.direct") + : `${node.hopsAway?.toString()} ${ + node.hopsAway ?? 0 > 1 + ? t("unit.hop.plural") + : t("unit.hops_one") + } ${t("nodesTable.connectionStatus.away")}` + : t("nodesTable.connectionStatus.unknown")} + {node?.viaMqtt === true + ? t("nodesTable.connectionStatus.viaMqtt") + : ""} + + ), + sortValue: node.hopsAway ?? Number.MAX_SAFE_INTEGER, + }, + { + content: ( + + {node.lastHeard === 0 + ?

{t("nodesTable.lastHeardStatus.never")}

+ : ( + + )} +
+ ), + sortValue: node.lastHeard, + }, + { + content: ( + + {node.user?.publicKey && node.user?.publicKey.length > 0 + ? + : } + + ), + sortValue: "", // Non-sortable column + }, + { + content: ( + + {node.snr} + {t("unit.dbm")}/ + {Math.min( + Math.max((node.snr + 10) * 5, 0), + 100, + )}%/{/* Percentage */} + {(node.snr + 10) * 5} + {t("unit.raw")} + + ), + sortValue: node.snr, + }, + { + content: ( + + {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]} + + ), + sortValue: Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0], + }, + { + content: {macAddress}, + sortValue: macAddress, + }, + ], + }; + }); + return ( <> {
[ -
- -
, -

handleNodeInfoDialog(node.num)} - onKeyUp={(evt) => { - evt.key === "Enter" && handleNodeInfoDialog(node.num); - }} - className="cursor-pointer underline ml-2 whitespace-break-spaces" - tabIndex={0} - role="button" - > - {node.user?.longName ?? numberToHexUnpadded(node.num)} -

, - - {node.hopsAway !== undefined - ? node?.viaMqtt === false && node.hopsAway === 0 - ? t("nodesTable.connectionStatus.direct") - : `${node.hopsAway?.toString()} ${ - node.hopsAway ?? 0 > 1 - ? t("unit.hop.plural") - : t("unit.hops_one") - } ${t("nodesTable.connectionStatus.away")}` - : t("nodesTable.connectionStatus.unknown")} - {node?.viaMqtt === true - ? t("nodesTable.connectionStatus.viaMqtt") - : ""} - , - - {node.lastHeard === 0 - ?

{t("nodesTable.lastHeardStatus.never")}

- : } -
, - - {node.user?.publicKey && node.user?.publicKey.length > 0 - ? - : } - , - - {node.snr} - {t("unit.dbm")}/ - {Math.min( - Math.max((node.snr + 10) * 5, 0), - 100, - )}%/{/* Percentage */} - {(node.snr + 10) * 5} - {t("unit.raw")} - , - - {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]} - , - - {base16 - .stringify(node.user?.macaddr ?? []) - .match(/.{1,2}/g) - ?.join(":") ?? t("unknown.shortName")} - , - ])} + headings={tableHeadings} + rows={tableRows} /> ; + message: ReturnType; + }; + i18n: ReturnType; +} + +export const rootRoute = createRootRouteWithContext()({ + component: () => , +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: Dashboard, + loader: () => { + // Redirect to the broadcast messages page on initial load + return redirect({ to: `/messages/broadcast/0`, replace: true }); + }, +}); + +const messagesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/messages", + component: MessagesPage, + beforeLoad: ({ params }) => { + const DEFAULT_CHANNEL = 0; + + if (Object.values(params).length === 0) { + throw redirect({ + to: `/messages/broadcast/${DEFAULT_CHANNEL}`, + replace: true, + }); + } + }, +}); + +const chatIdSchema = z.string().refine((val) => { + const num = Number(val); + if (isNaN(num) || !Number.isInteger(num)) { + return false; + } + + const isChannelId = num >= 0 && num <= 10; + const isNodeId = num >= 1000000000 && num <= 9999999999; + + return isChannelId || isNodeId; +}, { + message: "Chat ID must be a channel (0-10) or a valid node ID.", +}); + +export const messagesWithParamsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/messages/$type/$chatId", + component: MessagesPage, + parseParams: (params) => ({ + type: z.enum(["direct", "broadcast"], { + errorMap: () => ({ message: 'Type must be "direct" or "broadcast".' }), + }).parse(params.type), + chatId: chatIdSchema.parse(params.chatId), + }), +}); + +const mapRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/map", + component: MapPage, +}); + +const configRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/config", + component: ConfigPage, +}); + +const channelsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/channels", + component: ChannelsPage, +}); + +const nodesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/nodes", + component: NodesPage, +}); + +const dialogWithParamsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/dialog/$dialogId", + component: DialogManager, +}); + +const routeTree = rootRoute.addChildren([ + indexRoute, + messagesRoute, + messagesWithParamsRoute, + mapRoute, + configRoute, + channelsRoute, + nodesRoute, + dialogWithParamsRoute, +]); + +const router = createRouter({ + routeTree, + context: { + stores: { + app: {} as ReturnType, + message: {} as ReturnType, + }, + i18n: {} as ReturnType, + }, +}); +export { router }; diff --git a/src/tests/setupTests.ts b/packages/web/src/tests/setup.ts similarity index 68% rename from src/tests/setupTests.ts rename to packages/web/src/tests/setup.ts index 4211fb77..1c6afdc2 100644 --- a/src/tests/setupTests.ts +++ b/packages/web/src/tests/setup.ts @@ -5,28 +5,34 @@ import "@testing-library/jest-dom"; import "@testing-library/user-event"; import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import channelsEN from "@app/i18n/locales/en/channels.json" with { +import channelsEN from "@public/i18n/locales/en/channels.json" with { type: "json", }; -import commandPaletteEN from "@app/i18n/locales/en/commandPalette.json" with { +import commandPaletteEN from "@public/i18n/locales/en/commandPalette.json" with { type: "json", }; -import commonEN from "@app/i18n/locales/en/common.json" with { type: "json" }; -import deviceConfigEN from "@app/i18n/locales/en/deviceConfig.json" with { +import commonEN from "@public/i18n/locales/en/common.json" with { type: "json", }; -import moduleConfigEN from "@app/i18n/locales/en/moduleConfig.json" with { +import deviceConfigEN from "@public/i18n/locales/en/deviceConfig.json" with { type: "json", }; -import dashboardEN from "@app/i18n/locales/en/dashboard.json" with { +import moduleConfigEN from "@public/i18n/locales/en/moduleConfig.json" with { type: "json", }; -import dialogEN from "@app/i18n/locales/en/dialog.json" with { type: "json" }; -import messagesEN from "@app/i18n/locales/en/messages.json" with { +import dashboardEN from "@public/i18n/locales/en/dashboard.json" with { type: "json", }; -import nodesEN from "@app/i18n/locales/en/nodes.json" with { type: "json" }; -import uiEN from "@app/i18n/locales/en/ui.json" with { type: "json" }; +import dialogEN from "@public/i18n/locales/en/dialog.json" with { + type: "json", +}; +import messagesEN from "@public/i18n/locales/en/messages.json" with { + type: "json", +}; +import nodesEN from "@public/i18n/locales/en/nodes.json" with { + type: "json", +}; +import uiEN from "@public/i18n/locales/en/ui.json" with { type: "json" }; enableMapSet(); @@ -64,14 +70,11 @@ i18n .use(initReactI18next) .init({ lng: "en", - fallbackLng: "en", ns: appNamespaces, defaultNS: appDefaultNS, fallbackNS: appFallbackNS, - supportedLngs: ["en"], - resources: { en: { channels: channelsEN, diff --git a/packages/web/src/tests/test-utils.tsx b/packages/web/src/tests/test-utils.tsx new file mode 100644 index 00000000..7f6a209d --- /dev/null +++ b/packages/web/src/tests/test-utils.tsx @@ -0,0 +1,37 @@ +import { ReactElement } from "react"; +import { render, RenderOptions } from "@testing-library/react"; +import { + createMemoryHistory, + createRouter, + RouterProvider, +} from "@tanstack/react-router"; +import "@app/i18n-config.ts"; +import { routeTree } from "../routeTree.gen.ts"; + +import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; + +const Providers = () => { + const memoryHistory = createMemoryHistory({ + initialEntries: ["/"], + }); + + const router = createRouter({ + routeTree, + history: memoryHistory, + }); + + return ( + + + + ); +}; + +const renderWithProviders = ( + ui: ReactElement, + options?: Omit, +) => render(ui, { wrapper: Providers, ...options }); + +export * from "@testing-library/react"; + +export { renderWithProviders as render }; diff --git a/src/validation/channel.test.ts b/packages/web/src/validation/channel.test.ts similarity index 100% rename from src/validation/channel.test.ts rename to packages/web/src/validation/channel.test.ts diff --git a/src/validation/channel.ts b/packages/web/src/validation/channel.ts similarity index 100% rename from src/validation/channel.ts rename to packages/web/src/validation/channel.ts diff --git a/src/validation/config/bluetooth.ts b/packages/web/src/validation/config/bluetooth.ts similarity index 100% rename from src/validation/config/bluetooth.ts rename to packages/web/src/validation/config/bluetooth.ts diff --git a/src/validation/config/device.ts b/packages/web/src/validation/config/device.ts similarity index 100% rename from src/validation/config/device.ts rename to packages/web/src/validation/config/device.ts diff --git a/src/validation/config/display.ts b/packages/web/src/validation/config/display.ts similarity index 100% rename from src/validation/config/display.ts rename to packages/web/src/validation/config/display.ts diff --git a/src/validation/config/lora.ts b/packages/web/src/validation/config/lora.ts similarity index 100% rename from src/validation/config/lora.ts rename to packages/web/src/validation/config/lora.ts diff --git a/src/validation/config/network.ts b/packages/web/src/validation/config/network.ts similarity index 95% rename from src/validation/config/network.ts rename to packages/web/src/validation/config/network.ts index 26ba9c36..cdb486b0 100644 --- a/src/validation/config/network.ts +++ b/packages/web/src/validation/config/network.ts @@ -19,7 +19,7 @@ export const NetworkValidationSchema = z.object({ wifiEnabled: z.boolean(), wifiSsid: z.string().max(33), wifiPsk: z.string().max(64), - ntpServer: z.string().min(2).max(33), + ntpServer: z.string().min(0).max(33), ethEnabled: z.boolean(), addressMode: AddressModeEnum, ipv4Config: NetworkValidationIpV4ConfigSchema, diff --git a/src/validation/config/position.ts b/packages/web/src/validation/config/position.ts similarity index 100% rename from src/validation/config/position.ts rename to packages/web/src/validation/config/position.ts diff --git a/src/validation/config/power.ts b/packages/web/src/validation/config/power.ts similarity index 100% rename from src/validation/config/power.ts rename to packages/web/src/validation/config/power.ts diff --git a/src/validation/config/security.test.ts b/packages/web/src/validation/config/security.test.ts similarity index 94% rename from src/validation/config/security.test.ts rename to packages/web/src/validation/config/security.test.ts index 653baab4..428711c3 100644 --- a/src/validation/config/security.test.ts +++ b/packages/web/src/validation/config/security.test.ts @@ -53,7 +53,7 @@ describe("RawSecuritySchema", () => { if (!result.success) { expect( result.error.issues.some((i) => - i.message === "formValidation.adminKeyRequiredWhenManaged" + i.message === "formValidation.required.managed" ), ).toBe(true); } @@ -86,8 +86,6 @@ describe("ParsedSecuritySchema", () => { publicKey: validKey, adminKey: [validKey, new Uint8Array(), new Uint8Array()], }); - console.log(result); - expect(result.success).toBe(true); }); @@ -105,7 +103,7 @@ describe("ParsedSecuritySchema", () => { if (!result.success) { expect( result.error.issues.some((i) => - i.message === "formValidation.adminKeyRequiredWhenManaged" + i.message === "formValidation.required.managed" ), ).toBe(true); } diff --git a/src/validation/config/security.ts b/packages/web/src/validation/config/security.ts similarity index 94% rename from src/validation/config/security.ts rename to packages/web/src/validation/config/security.ts index 2053b085..4d7bd83a 100644 --- a/src/validation/config/security.ts +++ b/packages/web/src/validation/config/security.ts @@ -7,7 +7,7 @@ const { isValidKey, } = makePskHelpers([32]); // 256-bit -const isManagedRequiredMsg = "formValidation.adminKeyRequiredWhenManaged"; +const isManagedRequiredMsg = "formValidation.required.managed"; function makeSecuritySchema( keyMaker: (optional: boolean) => ZodType, diff --git a/src/validation/moduleConfig/ambientLighting.ts b/packages/web/src/validation/moduleConfig/ambientLighting.ts similarity index 100% rename from src/validation/moduleConfig/ambientLighting.ts rename to packages/web/src/validation/moduleConfig/ambientLighting.ts diff --git a/src/validation/moduleConfig/audio.ts b/packages/web/src/validation/moduleConfig/audio.ts similarity index 100% rename from src/validation/moduleConfig/audio.ts rename to packages/web/src/validation/moduleConfig/audio.ts diff --git a/src/validation/moduleConfig/cannedMessage.ts b/packages/web/src/validation/moduleConfig/cannedMessage.ts similarity index 100% rename from src/validation/moduleConfig/cannedMessage.ts rename to packages/web/src/validation/moduleConfig/cannedMessage.ts diff --git a/src/validation/moduleConfig/detectionSensor.ts b/packages/web/src/validation/moduleConfig/detectionSensor.ts similarity index 100% rename from src/validation/moduleConfig/detectionSensor.ts rename to packages/web/src/validation/moduleConfig/detectionSensor.ts diff --git a/src/validation/moduleConfig/externalNotification.ts b/packages/web/src/validation/moduleConfig/externalNotification.ts similarity index 100% rename from src/validation/moduleConfig/externalNotification.ts rename to packages/web/src/validation/moduleConfig/externalNotification.ts diff --git a/src/validation/moduleConfig/mqtt.ts b/packages/web/src/validation/moduleConfig/mqtt.ts similarity index 86% rename from src/validation/moduleConfig/mqtt.ts rename to packages/web/src/validation/moduleConfig/mqtt.ts index 638f076f..fe47c93d 100644 --- a/src/validation/moduleConfig/mqtt.ts +++ b/packages/web/src/validation/moduleConfig/mqtt.ts @@ -1,8 +1,8 @@ import { z } from "zod/v4"; export const MqttValidationMapReportSettingsSchema = z.object({ - publishIntervalSecs: z.number().optional(), - positionPrecision: z.number().optional(), + publishIntervalSecs: z.coerce.number().int(), + positionPrecision: z.coerce.number().int(), }); export const MqttValidationSchema = z.object({ diff --git a/src/validation/moduleConfig/neighborInfo.ts b/packages/web/src/validation/moduleConfig/neighborInfo.ts similarity index 100% rename from src/validation/moduleConfig/neighborInfo.ts rename to packages/web/src/validation/moduleConfig/neighborInfo.ts diff --git a/src/validation/moduleConfig/paxcounter.ts b/packages/web/src/validation/moduleConfig/paxcounter.ts similarity index 100% rename from src/validation/moduleConfig/paxcounter.ts rename to packages/web/src/validation/moduleConfig/paxcounter.ts diff --git a/src/validation/moduleConfig/rangeTest.ts b/packages/web/src/validation/moduleConfig/rangeTest.ts similarity index 100% rename from src/validation/moduleConfig/rangeTest.ts rename to packages/web/src/validation/moduleConfig/rangeTest.ts diff --git a/src/validation/moduleConfig/serial.ts b/packages/web/src/validation/moduleConfig/serial.ts similarity index 100% rename from src/validation/moduleConfig/serial.ts rename to packages/web/src/validation/moduleConfig/serial.ts diff --git a/src/validation/moduleConfig/storeForward.ts b/packages/web/src/validation/moduleConfig/storeForward.ts similarity index 100% rename from src/validation/moduleConfig/storeForward.ts rename to packages/web/src/validation/moduleConfig/storeForward.ts diff --git a/src/validation/moduleConfig/telemetry.ts b/packages/web/src/validation/moduleConfig/telemetry.ts similarity index 100% rename from src/validation/moduleConfig/telemetry.ts rename to packages/web/src/validation/moduleConfig/telemetry.ts diff --git a/src/validation/pskSchema.test.ts b/packages/web/src/validation/pskSchema.test.ts similarity index 100% rename from src/validation/pskSchema.test.ts rename to packages/web/src/validation/pskSchema.test.ts diff --git a/src/validation/pskSchema.ts b/packages/web/src/validation/pskSchema.ts similarity index 100% rename from src/validation/pskSchema.ts rename to packages/web/src/validation/pskSchema.ts diff --git a/src/validation/validate.ts b/packages/web/src/validation/validate.ts similarity index 100% rename from src/validation/validate.ts rename to packages/web/src/validation/validate.ts diff --git a/vercel.json b/packages/web/vercel.json similarity index 55% rename from vercel.json rename to packages/web/vercel.json index 24bca4be..2e6d8ee3 100644 --- a/vercel.json +++ b/packages/web/vercel.json @@ -1,5 +1,11 @@ { - "github": { "silent": true }, + "$schema": "https://openapi.vercel.sh/vercel.json", + "rewrites": [ + { + "source": "/((?!api|.*\\..*).*)", + "destination": "/index.html" + } + ], "headers": [ { "source": "/", @@ -14,5 +20,6 @@ } ] } - ] + ], + "relatedProjects": ["prj_G5osDwzRZBGplkfYTGFQziUGbLOm"] } diff --git a/packages/web/vite-env.d.ts b/packages/web/vite-env.d.ts new file mode 100644 index 00000000..484e03a4 --- /dev/null +++ b/packages/web/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +interface ImportMetaEnv { + readonly env: { + readonly VITE_COMMIT_HASH: string; + }; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/vite.config.ts b/packages/web/vite.config.ts similarity index 69% rename from vite.config.ts rename to packages/web/vite.config.ts index 83bba01f..7bef6349 100644 --- a/vite.config.ts +++ b/packages/web/vite.config.ts @@ -1,7 +1,5 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -import { VitePWA } from "vite-plugin-pwa"; -import { viteStaticCopy } from "vite-plugin-static-copy"; import { execSync } from "node:child_process"; import process from "node:process"; import path from "node:path"; @@ -17,26 +15,21 @@ try { export default defineConfig({ plugins: [ react(), - VitePWA({ - registerType: "autoUpdate", - strategies: "generateSW", - devOptions: { - enabled: false, - }, - workbox: { - cleanupOutdatedCaches: true, - sourcemap: true, - }, - }), - viteStaticCopy({ - targets: [ - { - src: "src/i18n/locales/**/*", - dest: "src/i18n/locales", - }, - ], - }), + // VitePWA({ + // registerType: "autoUpdate", + // strategies: "generateSW", + // devOptions: { + // enabled: true, + // }, + // workbox: { + // cleanupOutdatedCaches: true, + // sourcemap: true, + // }, + // }), ], + optimizeDeps: { + include: ["react/jsx-runtime"], + }, define: { "import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash), }, diff --git a/vitest.config.ts b/packages/web/vitest.config.ts similarity index 85% rename from vitest.config.ts rename to packages/web/vitest.config.ts index 537096e0..e36efbdd 100644 --- a/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -3,6 +3,8 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vitest/config"; import { enableMapSet } from "immer"; +import process from "node:process"; + enableMapSet(); export default defineConfig({ plugins: [ @@ -11,6 +13,7 @@ export default defineConfig({ resolve: { alias: { "@app": path.resolve(process.cwd(), "./src"), + "@public": path.resolve(process.cwd(), "./public"), "@core": path.resolve(process.cwd(), "./src/core"), "@pages": path.resolve(process.cwd(), "./src/pages"), "@components": path.resolve(process.cwd(), "./src/components"), @@ -25,6 +28,6 @@ export default defineConfig({ restoreMocks: true, root: path.resolve(process.cwd(), "./src"), include: ["**/*.{test,spec}.{ts,tsx}"], - setupFiles: ["./src/tests/setupTests.ts", "./src/core/utils/test.tsx"], + setupFiles: ["./src/tests/setup.ts"], }, }); diff --git a/public/Logo.svg b/public/Logo.svg deleted file mode 100644 index e6863f6a..00000000 --- a/public/Logo.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - -Created with Fabric.js 4.6.0 - - - - - - - - - - - - \ No newline at end of file diff --git a/public/Logo_Black.svg b/public/Logo_Black.svg deleted file mode 100644 index e0f9bb19..00000000 --- a/public/Logo_Black.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/public/Logo_White.svg b/public/Logo_White.svg deleted file mode 100644 index b1bcd575..00000000 --- a/public/Logo_White.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/public/chirpy.svg b/public/chirpy.svg deleted file mode 100644 index d215662d..00000000 --- a/public/chirpy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/diy.svg b/public/diy.svg deleted file mode 100644 index 823467ed..00000000 --- a/public/diy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-ht62-esp32c3-sx1262.svg b/public/heltec-ht62-esp32c3-sx1262.svg deleted file mode 100644 index c52534ef..00000000 --- a/public/heltec-ht62-esp32c3-sx1262.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-mesh-node-t114-case.svg b/public/heltec-mesh-node-t114-case.svg deleted file mode 100644 index b2abe639..00000000 --- a/public/heltec-mesh-node-t114-case.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-mesh-node-t114.svg b/public/heltec-mesh-node-t114.svg deleted file mode 100644 index 779a8f6a..00000000 --- a/public/heltec-mesh-node-t114.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-v3-case.svg b/public/heltec-v3-case.svg deleted file mode 100644 index 1b1d3c55..00000000 --- a/public/heltec-v3-case.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-v3.svg b/public/heltec-v3.svg deleted file mode 100644 index 13a5fa64..00000000 --- a/public/heltec-v3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-vision-master-e213.svg b/public/heltec-vision-master-e213.svg deleted file mode 100644 index 2c1cca09..00000000 --- a/public/heltec-vision-master-e213.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-vision-master-e290.svg b/public/heltec-vision-master-e290.svg deleted file mode 100644 index ca7d296a..00000000 --- a/public/heltec-vision-master-e290.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-vision-master-t190.svg b/public/heltec-vision-master-t190.svg deleted file mode 100644 index 55db34f9..00000000 --- a/public/heltec-vision-master-t190.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-wireless-paper-V1_0.svg b/public/heltec-wireless-paper-V1_0.svg deleted file mode 100644 index cb3f188d..00000000 --- a/public/heltec-wireless-paper-V1_0.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/public/heltec-wireless-paper.svg b/public/heltec-wireless-paper.svg deleted file mode 100644 index cb3f188d..00000000 --- a/public/heltec-wireless-paper.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/public/heltec-wireless-tracker-V1-0.svg b/public/heltec-wireless-tracker-V1-0.svg deleted file mode 100644 index a5392595..00000000 --- a/public/heltec-wireless-tracker-V1-0.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-wireless-tracker.svg b/public/heltec-wireless-tracker.svg deleted file mode 100644 index a5392595..00000000 --- a/public/heltec-wireless-tracker.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heltec-wsl-v3.svg b/public/heltec-wsl-v3.svg deleted file mode 100644 index 1741223e..00000000 --- a/public/heltec-wsl-v3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/icon.svg b/public/icon.svg deleted file mode 100644 index e6863f6a..00000000 --- a/public/icon.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - -Created with Fabric.js 4.6.0 - - - - - - - - - - - - \ No newline at end of file diff --git a/public/nano-g2-ultra.svg b/public/nano-g2-ultra.svg deleted file mode 100644 index 6dbe47af..00000000 --- a/public/nano-g2-ultra.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/pico.svg b/public/pico.svg deleted file mode 100644 index 82ce6526..00000000 --- a/public/pico.svg +++ /dev/null @@ -1,2956 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/promicro.svg b/public/promicro.svg deleted file mode 100644 index 3dc26021..00000000 --- a/public/promicro.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/rak-wismeshtap.svg b/public/rak-wismeshtap.svg deleted file mode 100644 index 34e77876..00000000 --- a/public/rak-wismeshtap.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/rak11310.svg b/public/rak11310.svg deleted file mode 100644 index 8f526a47..00000000 --- a/public/rak11310.svg +++ /dev/null @@ -1,2339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/rak2560.svg b/public/rak2560.svg deleted file mode 100644 index b8514f01..00000000 --- a/public/rak2560.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/rak4631.svg b/public/rak4631.svg deleted file mode 100644 index 6dc2957a..00000000 --- a/public/rak4631.svg +++ /dev/null @@ -1,3514 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/rak4631_case.svg b/public/rak4631_case.svg deleted file mode 100644 index a0b2bbb8..00000000 --- a/public/rak4631_case.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/rpipicow.svg b/public/rpipicow.svg deleted file mode 100644 index cb4b1f68..00000000 --- a/public/rpipicow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/seeed-sensecap-indicator.svg b/public/seeed-sensecap-indicator.svg deleted file mode 100644 index f7bf9db0..00000000 --- a/public/seeed-sensecap-indicator.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/seeed-xiao-s3.svg b/public/seeed-xiao-s3.svg deleted file mode 100644 index 04e97fe0..00000000 --- a/public/seeed-xiao-s3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/station-g2.svg b/public/station-g2.svg deleted file mode 100644 index 8d2e0aed..00000000 --- a/public/station-g2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/t-deck.svg b/public/t-deck.svg deleted file mode 100644 index cdc53c5d..00000000 --- a/public/t-deck.svg +++ /dev/null @@ -1 +0,0 @@ -QWERTYIUPOASDFGHKJLaltZXCVBMN \ No newline at end of file diff --git a/public/t-echo.svg b/public/t-echo.svg deleted file mode 100644 index e178a50f..00000000 --- a/public/t-echo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/t-watch-s3.svg b/public/t-watch-s3.svg deleted file mode 100644 index 19084c19..00000000 --- a/public/t-watch-s3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tbeam-s3-core.svg b/public/tbeam-s3-core.svg deleted file mode 100644 index f42e6d2c..00000000 --- a/public/tbeam-s3-core.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tbeam.svg b/public/tbeam.svg deleted file mode 100644 index cd0475c6..00000000 --- a/public/tbeam.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tlora-c6.svg b/public/tlora-c6.svg deleted file mode 100644 index 8b626638..00000000 --- a/public/tlora-c6.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tlora-t3s3-epaper.svg b/public/tlora-t3s3-epaper.svg deleted file mode 100644 index 6f2e8452..00000000 --- a/public/tlora-t3s3-epaper.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tlora-t3s3-v1.svg b/public/tlora-t3s3-v1.svg deleted file mode 100644 index 1f8847d4..00000000 --- a/public/tlora-t3s3-v1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tlora-v2-1-1_6.svg b/public/tlora-v2-1-1_6.svg deleted file mode 100644 index dbe36ef5..00000000 --- a/public/tlora-v2-1-1_6.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tlora-v2-1-1_8.svg b/public/tlora-v2-1-1_8.svg deleted file mode 100644 index dbe36ef5..00000000 --- a/public/tlora-v2-1-1_8.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tracker-t1000-e.svg b/public/tracker-t1000-e.svg deleted file mode 100644 index 6f7a06c9..00000000 --- a/public/tracker-t1000-e.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/unknown.svg b/public/unknown.svg deleted file mode 100644 index 1d2cd87b..00000000 --- a/public/unknown.svg +++ /dev/null @@ -1,160 +0,0 @@ - - diff --git a/public/wio-tracker-wm1110.svg b/public/wio-tracker-wm1110.svg deleted file mode 100644 index 15ace5c5..00000000 --- a/public/wio-tracker-wm1110.svg +++ /dev/null @@ -1 +0,0 @@ -LoRaWI FILEDRESETGNSSBLE \ No newline at end of file diff --git a/public/wm1110_dev_kit.svg b/public/wm1110_dev_kit.svg deleted file mode 100644 index 94aefe30..00000000 --- a/public/wm1110_dev_kit.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/BatteryStatus.tsx b/src/components/BatteryStatus.tsx deleted file mode 100644 index c80bb87e..00000000 --- a/src/components/BatteryStatus.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import { - BatteryFullIcon, - BatteryLowIcon, - BatteryMediumIcon, - PlugZapIcon, -} from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { DeviceMetrics } from "./types.ts"; - -interface BatteryStateConfig { - condition: (level: number) => boolean; - Icon: React.ElementType; - className: string; - text: (level: number) => string; -} - -interface BatteryStatusProps { - deviceMetrics?: DeviceMetrics | null; -} - -const getBatteryStates = ( - t: (key: string, options?: object) => string, -): BatteryStateConfig[] => { - return [ - { - condition: (level) => level > 100, - Icon: PlugZapIcon, - className: "text-gray-500", - text: () => t("batteryStatus.pluggedIn"), - }, - { - condition: (level) => level > 80, - Icon: BatteryFullIcon, - className: "text-green-500", - text: (level) => t("batteryStatus.charging", { level }), - }, - { - condition: (level) => level > 20, - Icon: BatteryMediumIcon, - className: "text-yellow-500", - text: (level) => t("batteryStatus.charging", { level }), - }, - { - condition: () => true, - Icon: BatteryLowIcon, - className: "text-red-500", - text: (level) => t("batteryStatus.charging", { level }), - }, - ]; -}; - -const getBatteryState = ( - level: number, - batteryStates: BatteryStateConfig[], -) => { - return batteryStates.find((state) => state.condition(level)); -}; - -const BatteryStatus: React.FC = ({ deviceMetrics }) => { - if ( - deviceMetrics?.batteryLevel === undefined || - deviceMetrics?.batteryLevel === null - ) { - return null; - } - - const { t } = useTranslation(); - const batteryStates = getBatteryStates(t); - - const { batteryLevel } = deviceMetrics; - const currentState = getBatteryState(batteryLevel, batteryStates) ?? - batteryStates[batteryStates.length - 1]; - - const BatteryIcon = currentState.Icon; - const iconClassName = currentState.className; - const statusText = currentState.text(batteryLevel); - - return ( -
- - {statusText} -
- ); -}; - -export default BatteryStatus; diff --git a/src/components/Dialog/LocationResponseDialog.tsx b/src/components/Dialog/LocationResponseDialog.tsx deleted file mode 100644 index bd376de3..00000000 --- a/src/components/Dialog/LocationResponseDialog.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useDevice } from "@core/stores/deviceStore.ts"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "../UI/Dialog.tsx"; -import type { Protobuf, Types } from "@meshtastic/core"; -import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; -import { useTranslation } from "react-i18next"; - -export interface LocationResponseDialogProps { - location: Types.PacketMetadata | undefined; - open: boolean; - onOpenChange: () => void; -} - -export const LocationResponseDialog = ({ - location, - open, - onOpenChange, -}: LocationResponseDialogProps) => { - const { t } = useTranslation("dialog"); - const { getNode } = useDevice(); - - const from = getNode(location?.from ?? 0); - const longName = from?.user?.longName ?? - (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName")); - const shortName = from?.user?.shortName ?? - (from - ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` - : t("unknown.shortName")); - - return ( - - - - - - {t("locationResponse.title", { - identifier: `${longName} (${shortName})`, - })} - - - -
- -

- {t("locationResponse.coordinates")} - - {location?.data.latitudeI / 1e7},{" "} - {location?.data.longitudeI / 1e7} - -

-

- {t("locationResponse.altitude")} - {location?.data.altitude} - {location?.data.altitde < 1 - ? t("unit.meter.one") - : t("unit.meter.plural")} -

-
-
-
-
-
- ); -}; diff --git a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.test.tsx b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.test.tsx deleted file mode 100644 index 0ba79fd1..00000000 --- a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx"; -import { useDevice } from "@core/stores/deviceStore.ts"; -import { useAppStore } from "@core/stores/appStore.ts"; -import type { Protobuf } from "@meshtastic/core"; - -vi.mock("@core/stores/deviceStore"); -vi.mock("@core/stores/appStore"); - -const mockUseDevice = vi.mocked(useDevice); -const mockUseAppStore = vi.mocked(useAppStore); - -vi.mock("@tanstack/react-router", () => ({ - useNavigate: vi.fn(), -})); - -describe("NodeDetailsDialog", () => { - const mockNode = { - num: 1234, - user: { - longName: "Test Node", - shortName: "TN", - hwModel: 1, - role: 1, - }, - lastHeard: 1697500000, - position: { - latitudeI: 450000000, - longitudeI: -750000000, - altitude: 200, - }, - deviceMetrics: { - airUtilTx: 50.123, - channelUtilization: 75.456, - batteryLevel: 88.789, - voltage: 4.2, - uptimeSeconds: 3600, - }, - } as unknown as Protobuf.Mesh.NodeInfo; - - beforeEach(() => { - vi.resetAllMocks(); - - mockUseDevice.mockReturnValue({ - getNode: (nodeNum: number) => { - if (nodeNum === 1234) { - return mockNode; - } - return undefined; - }, - }); - - mockUseAppStore.mockReturnValue({ - nodeNumDetails: 1234, - }); - }); - - it("renders node details correctly", () => { - render( {}} />); - - expect(screen.getByText(/Node Details for Test Node \(TN\)/i)) - .toBeInTheDocument(); - - expect(screen.getByText("Node Number: 1234")).toBeInTheDocument(); - expect(screen.getByText(/Node Hex: !/i)).toBeInTheDocument(); - expect(screen.getByText(/Last Heard:/i)).toBeInTheDocument(); - - expect(screen.getByText(/Coordinates:/i)).toBeInTheDocument(); - const link = screen.getByRole("link", { name: /^45, -75$/ }); - - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute( - "href", - expect.stringContaining("openstreetmap.org"), - ); - expect(screen.getByText(/Altitude: 200m/i)).toBeInTheDocument(); - - expect(screen.getByText(/Air TX utilization: 50.12%/i)).toBeInTheDocument(); - expect(screen.getByText(/Channel utilization: 75.46%/i)) - .toBeInTheDocument(); - expect(screen.getByText(/Battery level: 88.79%/i)).toBeInTheDocument(); - expect(screen.getByText(/Voltage: 4.20V/i)).toBeInTheDocument(); - expect(screen.getByText(/Uptime:/i)).toBeInTheDocument(); - - expect(screen.getByText(/All Raw Metrics:/i)).toBeInTheDocument(); - }); - - it("renders null if node is undefined", () => { - const requestedNodeNum = 5678; - - mockUseAppStore.mockReturnValue({ - nodeNumDetails: requestedNodeNum, - }); - - mockUseDevice.mockReturnValue({ - getNode: (nodeNum: number) => { - if (nodeNum === requestedNodeNum) { - return undefined; - } - if (nodeNum === 1234) { - return mockNode; - } - return undefined; - }, - }); - - const { container } = render( - {}} />, - ); - - expect(container.firstChild).toBeNull(); - expect(screen.queryByText(/Node Details for/i)).not.toBeInTheDocument(); - }); - - it("renders correctly when position is missing", () => { - const nodeWithoutPosition = { ...mockNode, position: undefined }; - mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutPosition }); - mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 }); - - render( {}} />); - - expect(screen.queryByText(/Coordinates:/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/Altitude:/i)).not.toBeInTheDocument(); - expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument(); - }); - - it("renders correctly when deviceMetrics are missing", () => { - const nodeWithoutMetrics = { ...mockNode, deviceMetrics: undefined }; - mockUseDevice.mockReturnValue({ getNode: () => nodeWithoutMetrics }); - mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 }); - - render( {}} />); - - expect(screen.queryByText(/Device Metrics:/i)).not.toBeInTheDocument(); - expect(screen.queryByText(/Air TX utilization:/i)).not.toBeInTheDocument(); - expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument(); - }); - - it("renders 'Never' for lastHeard when timestamp is 0", () => { - const nodeNeverHeard = { ...mockNode, lastHeard: 0 }; - mockUseDevice.mockReturnValue({ getNode: () => nodeNeverHeard }); - mockUseAppStore.mockReturnValue({ nodeNumDetails: 1234 }); - - render( {}} />); - - expect(screen.getByText(/Last Heard: Never/i)).toBeInTheDocument(); - }); -}); diff --git a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts deleted file mode 100644 index 4e10a7b5..00000000 --- a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { act, renderHook } from "@testing-library/react"; -import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; -import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; -import { useDevice } from "@core/stores/deviceStore.ts"; -import { useMessageStore } from "@core/stores/messageStore/index.ts"; - -vi.mock("@core/stores/messageStore", () => ({ - useMessageStore: vi.fn(() => ({ activeChat: "chat-123" })), -})); -vi.mock("@core/stores/deviceStore", () => ({ - useDevice: vi.fn(() => ({ - removeNode: vi.fn(), - setDialogOpen: vi.fn(), - getNodeError: vi.fn(), - clearNodeError: vi.fn(), - })), -})); - -describe("useRefreshKeysDialog Hook", () => { - let removeNodeMock: Mock; - let setDialogOpenMock: Mock; - let getNodeErrorMock: Mock; - let clearNodeErrorMock: Mock; - - beforeEach(() => { - vi.clearAllMocks(); - - removeNodeMock = vi.fn(); - setDialogOpenMock = vi.fn(); - getNodeErrorMock = vi.fn().mockReturnValue(undefined); - clearNodeErrorMock = vi.fn(); - - vi.mocked(useDevice).mockReturnValue({ - removeNode: removeNodeMock, - setDialogOpen: setDialogOpenMock, - getNodeError: getNodeErrorMock, - clearNodeError: clearNodeErrorMock, - }); - - vi.mocked(useMessageStore).mockReturnValue({ - activeChat: "chat-123", - }); - }); - - it("handleNodeRemove should remove the node and update dialog if there is an error", () => { - getNodeErrorMock.mockReturnValue({ node: "node-abc" }); - - const { result } = renderHook(() => useRefreshKeysDialog()); - act(() => { - result.current.handleNodeRemove(); - }); - - expect(getNodeErrorMock).toHaveBeenCalledTimes(1); - expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123"); - expect(clearNodeErrorMock).toHaveBeenCalledTimes(1); - expect(clearNodeErrorMock).toHaveBeenCalledWith("chat-123"); - expect(removeNodeMock).toHaveBeenCalledTimes(1); - expect(removeNodeMock).toHaveBeenCalledWith("node-abc"); - expect(setDialogOpenMock).toHaveBeenCalledTimes(1); - expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false); - }); - - it("handleNodeRemove should do nothing if there is no error", () => { - const { result } = renderHook(() => useRefreshKeysDialog()); - act(() => { - result.current.handleNodeRemove(); - }); - - expect(getNodeErrorMock).toHaveBeenCalledTimes(1); - expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123"); - expect(clearNodeErrorMock).not.toHaveBeenCalled(); - expect(removeNodeMock).not.toHaveBeenCalled(); - expect(setDialogOpenMock).not.toHaveBeenCalled(); - }); - - it("handleCloseDialog should close the dialog", () => { - const { result } = renderHook(() => useRefreshKeysDialog()); - - act(() => { - result.current.handleCloseDialog(); - }); - - expect(setDialogOpenMock).toHaveBeenCalledTimes(1); - expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false); - }); -}); diff --git a/src/components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.test.tsx b/src/components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.test.tsx deleted file mode 100644 index 1584f7e8..00000000 --- a/src/components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { fireEvent, render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx"; -import { - createMemoryHistory, - createRootRoute, - createRouter, - RouterProvider, -} from "@tanstack/react-router"; -import { eventBus } from "@core/utils/eventBus.ts"; -import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; - -const rootRoute = createRootRoute(); - -describe.skip("UnsafeRolesDialog", () => { - const mockDevice = { - setDialogOpen: vi.fn(), - }; - - const renderWithProviders = (ui: React.ReactNode) => { - const testRouter = createRouter({ - routeTree: rootRoute, - history: createMemoryHistory(), - }); - - return render( - - - {ui} - - , - ); - }; - - it("renders the dialog when open is true", () => { - renderWithProviders( - , - ); - - const dialog = screen.getByRole("dialog"); - expect(dialog).toBeInTheDocument(); - - expect(screen.getByText(/I have read the/i)).toBeInTheDocument(); - expect(screen.getByText(/understand the implications/i)) - .toBeInTheDocument(); - - const links = screen.getAllByRole("link"); - expect(links).toHaveLength(2); - expect(links[0]).toHaveTextContent("Device Role Documentation"); - expect(links[1]).toHaveTextContent("Choosing The Right Device Role"); - }); - - it("displays the correct links", () => { - renderWithProviders( - , - ); - - const docLink = screen.getByRole("link", { - name: /Device Role Documentation/i, - }); - const blogLink = screen.getByRole("link", { - name: /Choosing The Right Device Role/i, - }); - - expect(docLink).toHaveAttribute( - "to", - "https://meshtastic.org/docs/configuration/radio/device/", - ); - expect(blogLink).toHaveAttribute( - "to", - "https://meshtastic.org/blog/choosing-the-right-device-role/", - ); - }); - - it("does not allow confirmation until checkbox is checked", () => { - renderWithProviders( - , - ); - - const confirmButton = screen.getByRole("button", { name: /confirm/i }); - - expect(confirmButton).toBeDisabled(); - - const checkbox = screen.getByRole("checkbox"); - fireEvent.click(checkbox); - - expect(confirmButton).toBeEnabled(); - }); - - it("emits the correct event when closing via close button", () => { - const eventSpy = vi.spyOn(eventBus, "emit"); - renderWithProviders( - , - ); - - const dismissButton = screen.getByRole("button", { name: /close/i }); - fireEvent.click(dismissButton); - - expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { - action: "dismiss", - }); - }); - - it("emits the correct event when dismissing", () => { - const eventSpy = vi.spyOn(eventBus, "emit"); - renderWithProviders( - , - ); - - const dismissButton = screen.getByRole("button", { name: /dismiss/i }); - fireEvent.click(dismissButton); - - expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { - action: "dismiss", - }); - }); - - it("emits the correct event when confirming", () => { - const eventSpy = vi.spyOn(eventBus, "emit"); - renderWithProviders( - , - ); - - const checkbox = screen.getByRole("checkbox"); - const confirmButton = screen.getByRole("button", { name: /confirm/i }); - - fireEvent.click(checkbox); - fireEvent.click(confirmButton); - - expect(eventSpy).toHaveBeenCalledWith("dialog:unsafeRoles", { - action: "confirm", - }); - }); -}); diff --git a/src/components/Form/DynamicForm.test.tsx b/src/components/Form/DynamicForm.test.tsx deleted file mode 100644 index dd408b10..00000000 --- a/src/components/Form/DynamicForm.test.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { fireEvent, render, screen, waitFor } from "@core/utils/test.tsx"; -import { DynamicForm } from "./DynamicForm.tsx"; -import { z } from "zod/v4"; -import { useAppStore } from "@core/stores/appStore.ts"; - -vi.mock("react-i18next", () => ({ - useTranslation: () => ({ - t: (key: string | string[]) => (Array.isArray(key) ? key[0] : key), - }), -})); - -const addErrorMock = vi.fn(); -const removeErrorMock = vi.fn(); - -vi.mock("@core/stores/appStore.ts", () => ({ - useAppStore: () => ({ - addError: addErrorMock, - removeError: removeErrorMock, - }), -})); - -describe("DynamicForm", () => { - const schema = z.object({ - name: z.string().min(3, { message: "Too short" }), - }); - - const fieldGroups = [ - { - label: "Test Group", - description: "Testing validation", - fields: [ - { - type: "text", - id: "name", - name: "name", - label: "Name", - description: "Enter your name", - properties: {}, - }, - ], - }, - ]; - - it("shows validation error when input is too short", async () => { - render( - > - onSubmit={vi.fn()} - validationSchema={schema} - defaultValues={{ name: "" }} - fieldGroups={fieldGroups} - />, - ); - const input = screen.getByLabelText("Name") as HTMLInputElement; - - fireEvent.input(input, { target: { value: "ab" } }); - - const error = await screen.findByText( - "formValidation.tooSmall.string", - ); - expect(error).toBeVisible(); - }); - - it("clears validation error when input becomes valid", async () => { - render( - > - onSubmit={vi.fn()} - validationSchema={schema} - defaultValues={{ name: "" }} - fieldGroups={fieldGroups} - />, - ); - const input = screen.getByLabelText("Name") as HTMLInputElement; - - fireEvent.input(input, { target: { value: "ab" } }); - expect( - await screen.findByText("formValidation.tooSmall.string"), - ).toBeVisible(); - - fireEvent.input(input, { target: { value: "abcd" } }); - await waitFor(() => - expect( - screen.queryByText("formValidation.tooSmall.string"), - ).toBeNull() - ); - }); - - it("calls onSubmit when form is valid onChange", async () => { - const onSubmit = vi.fn(); - render( - > - onSubmit={onSubmit} - validationSchema={schema} - defaultValues={{ name: "" }} - fieldGroups={fieldGroups} - />, - ); - - const input = screen.getByLabelText("Name") as HTMLInputElement; - - fireEvent.input(input, { target: { value: "ab" } }); - expect( - await screen.findByText("formValidation.tooSmall.string"), - ).toBeVisible(); - - fireEvent.input(input, { target: { value: "abcd" } }); - - await waitFor(() => { - expect(onSubmit).toHaveBeenCalledTimes(1); - }); - - expect(onSubmit).toHaveBeenCalledWith( - { name: "abcd" }, - expect.any(Object), - ); - }); - - it("renders a button and only calls onSubmit on click with submitType='onSubmit'", async () => { - const onSubmit = vi.fn(); - render( - > - onSubmit={onSubmit} - submitType="onSubmit" - hasSubmitButton - validationSchema={schema} - defaultValues={{ name: "" }} - fieldGroups={fieldGroups} - />, - ); - - const btn = screen.getByRole("button", { name: /submit/i }); - expect(btn).toBeInTheDocument(); - - fireEvent.input(screen.getByLabelText("Name"), { target: { value: "ab" } }); - await screen.findByText("formValidation.tooSmall.string"); - fireEvent.click(btn); - expect(onSubmit).not.toHaveBeenCalled(); - - fireEvent.input(screen.getByLabelText("Name"), { - target: { value: "abcd" }, - }); - await waitFor(() => - expect(screen.queryByText("formValidation.tooSmall.string")).toBeNull() - ); - fireEvent.click(btn); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit).toHaveBeenCalledWith({ name: "abcd" }, expect.any(Object)); - }); - - it("renders defaultValues correctly", () => { - render( - - onSubmit={vi.fn()} - // no validationSchema - defaultValues={{ name: "Alice" }} - fieldGroups={[ - { - label: "Group", - description: "", - fields: [ - { - type: "text", - name: "name", - label: "Name", - description: "", - properties: {}, - }, - ], - }, - ]} - />, - ); - const input = screen.getByLabelText("Name") as HTMLInputElement; - expect(input.value).toBe("Alice"); - }); - - it("toggles disabled state based on disabledBy rules", async () => { - const schema = z.object({ - enable: z.boolean(), - follow: z.string(), - }); - render( - > - onSubmit={vi.fn()} - validationSchema={schema} - defaultValues={{ enable: false, follow: "" }} - fieldGroups={[ - { - label: "Group", - description: "", - fields: [ - { - type: "toggle", - name: "enable", - label: "enable", - description: "", - }, - { - type: "text", - name: "follow", - label: "follow", - description: "", - disabledBy: [{ fieldName: "enable" }], - properties: {}, - }, - ], - }, - ]} - />, - ); - const enable = screen.getByRole("switch", { - name: "enable", - }) as HTMLInputElement; - - const follow = screen.getByLabelText("follow") as HTMLInputElement; - await waitFor(() => { - expect(enable.getAttribute("aria-checked")).toBe("false"); - expect(follow).toBeDisabled(); - }); - - fireEvent.click(enable); - await waitFor(() => { - expect(enable.getAttribute("aria-checked")).toBe("true"); - expect(follow).not.toBeDisabled(); - }); - }); - - it("always calls onSubmit onChange when no validationSchema is provided", async () => { - const onSubmit = vi.fn(); - render( - - onSubmit={onSubmit} - // no validationSchema - defaultValues={{ foo: "" }} - fieldGroups={[ - { - label: "G", - description: "", - fields: [ - { - type: "text", - name: "foo", - label: "Foo", - description: "", - properties: {}, - }, - ], - }, - ]} - />, - ); - const input = screen.getByLabelText("Foo") as HTMLInputElement; - fireEvent.input(input, { target: { value: "bar" } }); - - await waitFor(() => { - expect(onSubmit).toHaveBeenCalledTimes(1); - expect(onSubmit).toHaveBeenCalledWith({ foo: "bar" }, expect.any(Object)); - }); - }); - - it("syncs errors to appStore when formId is set", async () => { - const { addError, removeError } = useAppStore(); - const schema = z.object({ foo: z.string().min(2) }); - const groups = [ - { - label: "G", - description: "", - fields: [ - { - type: "text", - name: "foo", - label: "Foo", - description: "", - properties: {}, - }, - ], - }, - ]; - - render( - > - onSubmit={vi.fn()} - formId="myForm" - validationSchema={schema} - defaultValues={{ foo: "" }} - fieldGroups={groups} - />, - ); - const input = screen.getByLabelText("Foo") as HTMLInputElement; - - fireEvent.input(input, { target: { value: "a" } }); - await screen.findByText(/tooSmall/i); - - expect(addError).toHaveBeenCalledWith("foo", ""); - expect(addError).toHaveBeenCalledWith("myForm", ""); - - fireEvent.input(input, { target: { value: "abc" } }); - await waitFor(() => { - expect(removeError).toHaveBeenCalledWith("foo"); - expect(removeError).toHaveBeenCalledWith("myForm"); - }); - }); -}); diff --git a/src/components/PageComponents/Config/Device/Device.test.tsx b/src/components/PageComponents/Config/Device/Device.test.tsx deleted file mode 100644 index 538f6a5d..00000000 --- a/src/components/PageComponents/Config/Device/Device.test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { Device } from "@components/PageComponents/Config/Device/index.tsx"; -import { useDevice } from "@core/stores/deviceStore.ts"; -import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts"; -import { Protobuf } from "@meshtastic/core"; - -vi.mock("@core/stores/deviceStore.ts", () => ({ - useDevice: vi.fn(), -})); - -vi.mock("@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts", () => ({ - useUnsafeRolesDialog: vi.fn(), -})); - -// Mock the DynamicForm component since we're testing the Device component, -// not the DynamicForm implementation -vi.mock("@components/Form/DynamicForm", () => ({ - DynamicForm: vi.fn(({ onSubmit }) => { - // Render a simplified version of the form for testing - return ( -
- - -
- ); - }), -})); - -describe("Device component", () => { - const setWorkingConfigMock = vi.fn(); - const validateRoleSelectionMock = vi.fn(); - const mockDeviceConfig = { - role: "CLIENT", - buttonGpio: 0, - buzzerGpio: 0, - rebroadcastMode: "ALL", - nodeInfoBroadcastSecs: 300, - doubleTapAsButtonPress: false, - disableTripleClick: false, - ledHeartbeatDisabled: false, - }; - - beforeEach(() => { - vi.resetAllMocks(); - - // Mock the useDevice hook - useDevice.mockReturnValue({ - config: { - device: mockDeviceConfig, - }, - setWorkingConfig: setWorkingConfigMock, - }); - - // Mock the useUnsafeRolesDialog hook - validateRoleSelectionMock.mockResolvedValue(true); - useUnsafeRolesDialog.mockReturnValue({ - validateRoleSelection: validateRoleSelectionMock, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it("should render the Device form", () => { - render(); - expect(screen.getByTestId("dynamic-form")).toBeInTheDocument(); - }); - - it("should use the validateRoleSelection from the unsafe roles hook", () => { - render(); - expect(useUnsafeRolesDialog).toHaveBeenCalled(); - }); - - it("should call setWorkingConfig when form is submitted", async () => { - render(); - - fireEvent.click(screen.getByTestId("submit-button")); - - await waitFor(() => { - expect(setWorkingConfigMock).toHaveBeenCalledWith( - expect.objectContaining({ - payloadVariant: { - case: "device", - value: expect.objectContaining({ role: "CLIENT" }), - }, - }), - ); - }); - }); - - it("should create config with proper structure", async () => { - render(); - - // Simulate form submission - fireEvent.click(screen.getByTestId("submit-button")); - - await waitFor(() => { - expect(setWorkingConfigMock).toHaveBeenCalledWith( - expect.objectContaining({ - payloadVariant: { - case: "device", - value: expect.any(Object), - }, - }), - ); - }); - }); -}); diff --git a/src/components/PageComponents/Config/Network/Network.test.tsx b/src/components/PageComponents/Config/Network/Network.test.tsx deleted file mode 100644 index c15ec1ab..00000000 --- a/src/components/PageComponents/Config/Network/Network.test.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { Network } from "@components/PageComponents/Config/Network/index.tsx"; -import { useDevice } from "@core/stores/deviceStore.ts"; -import { Protobuf } from "@meshtastic/core"; - -vi.mock("@core/stores/deviceStore", () => ({ - useDevice: vi.fn(), -})); - -vi.mock("@components/Form/DynamicForm", async () => { - const React = await import("react"); - const { useState } = React; - - return { - DynamicForm: ({ onSubmit, defaultValues }) => { - const [wifiEnabled, setWifiEnabled] = useState( - defaultValues.wifiEnabled ?? false, - ); - const [ssid, setSsid] = useState(defaultValues.wifiSsid ?? ""); - const [psk, setPsk] = useState(defaultValues.wifiPsk ?? ""); - - return ( -
{ - e.preventDefault(); - onSubmit({ - ...defaultValues, - wifiEnabled, - wifiSsid: ssid, - wifiPsk: psk, - }); - }} - data-testid="dynamic-form" - > - setWifiEnabled(e.target.checked)} - /> - setSsid(e.target.value)} - disabled={!wifiEnabled} - /> - setPsk(e.target.value)} - disabled={!wifiEnabled} - /> - - - ); - }, - }; -}); - -describe("Network component", () => { - const setWorkingConfigMock = vi.fn(); - const mockNetworkConfig = { - wifiEnabled: false, - wifiSsid: "", - wifiPsk: "", - ntpServer: "", - ethEnabled: false, - addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, - ipv4Config: { - ip: 0, - gateway: 0, - subnet: 0, - dns: 0, - }, - enabledProtocols: - Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST, - rsyslogServer: "", - }; - - beforeEach(() => { - vi.resetAllMocks(); - - useDevice.mockReturnValue({ - config: { - network: mockNetworkConfig, - }, - setWorkingConfig: setWorkingConfigMock, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it("should render the Network form", () => { - render(); - expect(screen.getByTestId("dynamic-form")).toBeInTheDocument(); - }); - - it("should disable SSID and PSK fields when wifi is off", () => { - render(); - expect(screen.getByLabelText("SSID")).toBeDisabled(); - expect(screen.getByLabelText("PSK")).toBeDisabled(); - }); - - it("should enable SSID and PSK when wifi is toggled on", async () => { - render(); - const toggle = screen.getByLabelText("WiFi Enabled"); - screen.debug(); - - fireEvent.click(toggle); // turns wifiEnabled = true - - await waitFor(() => { - expect(screen.getByLabelText("SSID")).not.toBeDisabled(); - expect(screen.getByLabelText("PSK")).not.toBeDisabled(); - }); - }); - - it("should call setWorkingConfig with the right structure on submit", async () => { - render(); - - fireEvent.click(screen.getByTestId("submit-button")); - - await waitFor(() => { - expect(setWorkingConfigMock).toHaveBeenCalledWith( - expect.objectContaining({ - payloadVariant: { - case: "network", - value: expect.objectContaining({ - wifiEnabled: false, - wifiSsid: "", - wifiPsk: "", - ntpServer: "", - ethEnabled: false, - rsyslogServer: "", - }), - }, - }), - ); - }); - }); - - it("should submit valid data after enabling wifi and entering SSID and PSK", async () => { - render(); - fireEvent.click(screen.getByLabelText("WiFi Enabled")); - - fireEvent.change(screen.getByLabelText("SSID"), { - target: { value: "MySSID" }, - }); - - fireEvent.change(screen.getByLabelText("PSK"), { - target: { value: "MySecretPSK" }, - }); - - fireEvent.click(screen.getByTestId("submit-button")); - - await waitFor(() => { - expect(setWorkingConfigMock).toHaveBeenCalledWith( - expect.objectContaining({ - payloadVariant: { - case: "network", - value: expect.objectContaining({ - wifiEnabled: true, - wifiSsid: "MySSID", - wifiPsk: "MySecretPSK", - }), - }, - }), - ); - }); - }); -}); diff --git a/src/components/generic/Filter/useFilterNode.ts b/src/components/generic/Filter/useFilterNode.ts deleted file mode 100644 index da3b7347..00000000 --- a/src/components/generic/Filter/useFilterNode.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Protobuf } from "@meshtastic/core"; -import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; -import { useMemo } from "react"; - -export type FilterState = { - nodeName: string; - hopsAway: [number, number]; - lastHeard: [number, number]; - isFavorite: boolean | undefined; // undefined -> don't filter - viaMqtt: boolean | undefined; // undefined -> don't filter - snr: [number, number]; - channelUtilization: [number, number]; - airUtilTx: [number, number]; - batteryLevel: [number, number]; - voltage: [number, number]; - role: (Protobuf.Config.Config_DeviceConfig_Role)[]; - hwModel: (Protobuf.Mesh.HardwareModel)[]; -}; - -export function useFilterNode() { - const defaultFilterValues = useMemo(() => ({ - nodeName: "", - hopsAway: [0, 7], - lastHeard: [0, 864000], // 0-10 days - isFavorite: undefined, - viaMqtt: undefined, - snr: [-20, 10], - channelUtilization: [0, 100], - airUtilTx: [0, 100], - batteryLevel: [0, 101], - voltage: [0, 5], - role: Object.values(Protobuf.Config.Config_DeviceConfig_Role).filter( - (v): v is Protobuf.Config.Config_DeviceConfig_Role => - typeof v === "number", - ), - hwModel: Object.values(Protobuf.Mesh.HardwareModel).filter( - (v): v is Protobuf.Mesh.HardwareModel => typeof v === "number", - ), - }), []); - - function nodeFilter( - node: Protobuf.Mesh.NodeInfo, - filterOverrides?: Partial, - ): boolean { - const filterState: FilterState = { - ...defaultFilterValues, - ...filterOverrides, - }; - - if (!node.user) return false; - - const nodeName = filterState.nodeName.toLowerCase(); - if ( - !( - node.user?.shortName.toLowerCase().includes(nodeName) || - node.user?.longName.toLowerCase().includes(nodeName) || - node?.num.toString().includes(nodeName) || - numberToHexUnpadded(node?.num).includes( - nodeName.replace(/!/g, ""), - ) - ) - ) return false; - - const hops = node?.hopsAway ?? 7; - if (hops < filterState.hopsAway[0] || hops > filterState.hopsAway[1]) { - return false; - } - - const secondsAgo = Date.now() / 1000 - (node?.lastHeard ?? 0); - if ( - secondsAgo < filterState.lastHeard[0] || - ( - secondsAgo > filterState.lastHeard[1] && - filterState.lastHeard[1] !== defaultFilterValues.lastHeard[1] - ) - ) return false; - - if ( - typeof filterState.isFavorite !== "undefined" && - node.isFavorite !== filterState.isFavorite - ) return false; - - if ( - typeof filterState.viaMqtt !== "undefined" && - node.viaMqtt !== filterState.viaMqtt - ) return false; - - const snr = node?.snr ?? -20; - if ( - snr < filterState.snr[0] || - snr > filterState.snr[1] - ) return false; - - const channelUtilization = node?.deviceMetrics?.channelUtilization ?? 0; - if ( - channelUtilization < filterState.channelUtilization[0] || - channelUtilization > filterState.channelUtilization[1] - ) return false; - - const airUtilTx = node?.deviceMetrics?.airUtilTx ?? 0; - if ( - airUtilTx < filterState.airUtilTx[0] || - airUtilTx > filterState.airUtilTx[1] - ) return false; - - const batt = node?.deviceMetrics?.batteryLevel ?? 101; - if ( - batt < filterState.batteryLevel[0] || - batt > filterState.batteryLevel[1] - ) return false; - - const voltage = node?.deviceMetrics?.voltage ?? 0; - if ( - voltage < filterState.voltage[0] || - voltage > filterState.voltage[1] - ) return false; - - const role: Protobuf.Config.Config_DeviceConfig_Role = node.user?.role ?? - Protobuf.Config.Config_DeviceConfig_Role.CLIENT; - if (!filterState.role.includes(role)) return false; - - const hwModel: Protobuf.Mesh.HardwareModel = node.user?.hwModel ?? - Protobuf.Mesh.HardwareModel.UNSET; - if (!filterState.hwModel.includes(hwModel)) return false; - - // All conditions are true - return true; - } - - // deno-lint-ignore no-explicit-any - function shallowEqualArray(a: any[], b: any[]) { - return a.length === b.length && a.every((v, i) => v === b[i]); - } - - function isFilterDirty( - current: FilterState, - overrides?: Partial, - ): boolean { - const base: FilterState = overrides - ? { ...defaultFilterValues, ...overrides } - : defaultFilterValues; - - return (Object.keys(base) as (keyof FilterState)[]).some((key) => { - const curr = current[key]; - const def = base[key]; - return Array.isArray(def) && Array.isArray(curr) - ? !shallowEqualArray(curr, def) - : curr !== def; - }); - } - - return { nodeFilter, defaultFilterValues, isFilterDirty }; -} diff --git a/src/components/generic/Table/index.test.tsx b/src/components/generic/Table/index.test.tsx deleted file mode 100644 index 727357f4..00000000 --- a/src/components/generic/Table/index.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { fireEvent, render, screen } from "@testing-library/react"; -import { Table } from "@components/generic/Table/index.tsx"; -import { TimeAgo } from "@components/generic/TimeAgo.tsx"; -import { Mono } from "@components/generic/Mono.tsx"; -// @ts-types="react" - -describe("Generic Table", () => { - it("Can render an empty table.", () => { - render( -
, - ); - expect(screen.getByRole("table")).toBeInTheDocument(); - }); - - it("Can render a table with headers and no rows.", async () => { - render( -
, - ); - await screen.findByRole("table"); - expect(screen.getAllByRole("columnheader")).toHaveLength(9); - }); - - // A simplified version of the rows in pages/Nodes.tsx for testing purposes - const mockDevicesWithShortNameAndConnection = [ - { - user: { shortName: "TST1" }, - hopsAway: 1, - lastHeard: Date.now() + 1000, - viaMqtt: false, - }, - { - user: { shortName: "TST2" }, - hopsAway: 0, - lastHeard: Date.now() + 4000, - viaMqtt: true, - }, - { - user: { shortName: "TST3" }, - hopsAway: 4, - lastHeard: Date.now(), - viaMqtt: false, - }, - { - user: { shortName: "TST4" }, - hopsAway: 3, - lastHeard: Date.now() + 2000, - viaMqtt: true, - }, - ]; - - const mockRows = mockDevicesWithShortNameAndConnection.map((node) => [ -

{node.user.shortName}

, - - - , - - {node.lastHeard !== 0 - ? node.viaMqtt === false && node.hopsAway === 0 - ? "Direct" - : `${node.hopsAway?.toString()} ${ - node.hopsAway ?? 0 > 1 ? "hops" : "hop" - } away` - : "-"} - {node.viaMqtt === true ? ", via MQTT" : ""} - , - ]); - - it("Can sort rows appropriately.", async () => { - render( -
, - ); - const renderedTable = await screen.findByRole("table"); - const columnHeaders = screen.getAllByRole("columnheader"); - expect(columnHeaders).toHaveLength(3); - - // Will be sorted "Last heard" "asc" by default - expect( - [...renderedTable.querySelectorAll("[data-testshortname]")] - .map((el) => el.textContent) - .map((v) => v?.trim()) - .join(","), - ) - .toMatch("TST2,TST4,TST1,TST3"); - - fireEvent.click(columnHeaders[0]); - - // Re-sort by Short Name asc - expect( - [...renderedTable.querySelectorAll("[data-testshortname]")] - .map((el) => el.textContent) - .map((v) => v?.trim()) - .join(","), - ) - .toMatch("TST1,TST2,TST3,TST4"); - - fireEvent.click(columnHeaders[0]); - - // Re-sort by Short Name desc - expect( - [...renderedTable.querySelectorAll("[data-testshortname]")] - .map((el) => el.textContent) - .map((v) => v?.trim()) - .join(","), - ) - .toMatch("TST4,TST3,TST2,TST1"); - - fireEvent.click(columnHeaders[2]); - - // Re-sort by Hops Away - expect( - [...renderedTable.querySelectorAll("[data-testshortname]")] - .map((el) => el.textContent) - .map((v) => v?.trim()) - .join(","), - ) - .toMatch("TST2,TST1,TST4,TST3"); - }); -}); diff --git a/src/components/generic/Table/index.tsx b/src/components/generic/Table/index.tsx deleted file mode 100755 index ae66a730..00000000 --- a/src/components/generic/Table/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import { useState } from "react"; -import React from "react"; -import { cn } from "@core/utils/cn.ts"; - -interface FavoriteIconProps { - showFavorite: boolean; -} - -interface AvatarCellProps { - children: React.ReactElement; -} - -export interface TableProps { - headings: Heading[]; - rows: React.ReactElement[][]; -} - -export interface Heading { - title: string; - type: "blank" | "normal"; - sortable: boolean; -} - -function numericHops(hopsAway: string | unknown): number { - if (typeof hopsAway !== "string") { - return Number.MAX_SAFE_INTEGER; - } - if (hopsAway.match(/direct/i)) { - return 0; - } - const match = hopsAway.match(/(\d+)\s+hop/i); - return Number(match?.[1] ?? Number.MAX_SAFE_INTEGER); -} - -export const Table = ({ headings, rows }: TableProps) => { - const [sortColumn, setSortColumn] = useState("Last Heard"); - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); - - const headingSort = (title: string) => { - if (sortColumn === title) { - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - } else { - setSortColumn(title); - setSortOrder("asc"); - } - }; - - const getElement = (cell: React.ReactNode): React.ReactElement | null => { - if (!React.isValidElement(cell)) { - return null; - } - if (cell.type === React.Fragment) { - const childrenArray = React.Children.toArray(cell.props.children); - const firstElement = childrenArray.find((child) => - React.isValidElement(child) - ); - return (firstElement as React.ReactElement) ?? null; - } - // If not a fragment, return the element itself - return cell; - }; - - const sortedRows = rows.slice().sort((a, b) => { - if (!sortColumn) return 0; - - const columnIndex = headings.findIndex((h) => h.title === sortColumn); - if (columnIndex === -1) return 0; - - const elementA = getElement(a[columnIndex]); - const elementB = getElement(b[columnIndex]); - - // Avatar contains the prop showFavorite which indicates isFavorite - const favA = a[0]?.props?.children?.props?.showFavorite ?? false; - const favB = b[0]?.props?.children?.props?.showFavorite ?? false; - - // Always put favorites at the top - if (favA !== favB) return favA ? -1 : 1; - - if (sortColumn === "Last Heard") { - const aTimestamp = elementA?.props?.children?.props?.timestamp ?? 0; - const bTimestamp = elementB?.props?.children?.props?.timestamp ?? 0; - if (aTimestamp < bTimestamp) return sortOrder === "asc" ? -1 : 1; - if (aTimestamp > bTimestamp) return sortOrder === "asc" ? 1 : -1; - return 0; - } - - if (sortColumn === "Connection") { - const aHopsStr = elementA?.props?.children[0]; - const bHopsStr = elementB?.props?.children[0]; - const aNumHops = numericHops(aHopsStr); - const bNumHops = numericHops(bHopsStr); - if (aNumHops < bNumHops) return sortOrder === "asc" ? -1 : 1; - if (aNumHops > bNumHops) return sortOrder === "asc" ? 1 : -1; - return 0; - } - - const aValue = elementA?.props?.children; - const bValue = elementB?.props?.children; - const valA = aValue ?? ""; - const valB = bValue ?? ""; - - // Ensure consistent comparison for potentially different types - const compareA = typeof valA === "string" || typeof valA === "number" - ? valA - : String(valA); - const compareB = typeof valB === "string" || typeof valB === "number" - ? valB - : String(valB); - - if (compareA < compareB) return sortOrder === "asc" ? -1 : 1; - if (compareA > compareB) return sortOrder === "asc" ? 1 : -1; - return 0; - }); - - return ( -
- - - {headings.map((heading) => ( - - ))} - - - - {sortedRows.map((row) => { - const firstCellKey = - (React.isValidElement(row[0]) && row[0].key !== null) - ? String(row[0].key) - : null; - const rowKey = firstCellKey ?? Math.random().toString(); // Use random only as last resort - - const isFavorite = row[0]?.props?.children?.props?.showFavorite ?? - false; - return ( - - {row.map((item, cellIndex) => { - const cellKey = `${rowKey}_${cellIndex}`; - return cellIndex === 0 - ? ( - - ) - : ( - - ); - })} - - ); - })} - -
heading.sortable && headingSort(heading.title)} - onKeyUp={(e) => { - if ( - heading.sortable && (e.key === "Enter" || e.key === " ") - ) { - headingSort(heading.title); - } - }} - tabIndex={heading.sortable ? 0 : -1} - role="columnheader" - aria-sort={sortColumn === heading.title - ? sortOrder === "asc" ? "ascending" : "descending" - : "none"} - > -
- {heading.title} - {heading.sortable && sortColumn === heading.title && ( - sortOrder === "asc" - ?
-
- {item} - - {item} -
- ); -}; diff --git a/src/components/generic/TimeAgo.tsx b/src/components/generic/TimeAgo.tsx deleted file mode 100755 index 652cd364..00000000 --- a/src/components/generic/TimeAgo.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - Tooltip, - TooltipContent, - TooltipPortal, - TooltipProvider, - TooltipTrigger, -} from "@radix-ui/react-tooltip"; - -export interface TimeAgoProps { - timestamp: number; -} - -const getTimeAgo = ( - unixTimestamp: number, - locale: Intl.LocalesArgument = "en", -): string => { - const timestamp = new Date(unixTimestamp); - const diff = (new Date().getTime() - timestamp.getTime()) / 1000; - - const minutes = Math.floor(diff / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - const months = Math.floor(days / 30); - const years = Math.floor(months / 12); - const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); - - if (years > 0) { - return rtf.format(0 - years, "year"); - } - if (months > 0) { - return rtf.format(0 - months, "month"); - } - if (days > 0) { - return rtf.format(0 - days, "day"); - } - if (hours > 0) { - return rtf.format(0 - hours, "hour"); - } - if (minutes > 0) { - return rtf.format(0 - minutes, "minute"); - } - return rtf.format(Math.floor(0 - diff), "second"); -}; - -export const TimeAgo = ({ timestamp }: TimeAgoProps) => { - return ( - - - - {getTimeAgo(timestamp)} - - - - {new Date(timestamp).toLocaleString()} - - - - - ); -}; diff --git a/src/core/hooks/useLang.ts b/src/core/hooks/useLang.ts deleted file mode 100644 index 62705bc3..00000000 --- a/src/core/hooks/useLang.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { LangCode } from "@app/i18n/config.ts"; -import useLocalStorage from "./useLocalStorage.ts"; - -/** - * Hook to set the i18n language - * - * @returns The `set` function - */ -const STORAGE_KEY = "language"; - -type LanguageState = { - language: string; -}; -function useLang() { - const { i18n } = useTranslation(); - const [_, setLanguage] = useLocalStorage( - STORAGE_KEY, - null, - ); - - const regionNames = useMemo(() => { - return new Intl.DisplayNames(i18n.language, { - type: "region", - fallback: "none", - style: "long", - }); - }, [i18n.language]); - - const collator = useMemo(() => { - return new Intl.Collator(i18n.language, {}); - }, [i18n.language]); - - /** - * Sets the i18n language. - * - * @param lng - The language tag to set - */ - const set = useCallback( - async (lng: LangCode, persist = true) => { - if (i18n.language === lng) { - return; - } - console.info("set language:", lng); - if (persist) { - try { - setLanguage({ language: lng }); - } catch (e) { - console.warn(e); - } - await i18n.changeLanguage(lng); - } - }, - [i18n], - ); - - /** - * Get the localized country name - * - * @param code - Two-letter country code - */ - const getCountryName = useCallback( - (code: LangCode) => { - let name = null; - try { - name = regionNames.of(code); - } catch (e) { - console.warn(e); - } - return name; - }, - [regionNames], - ); - - /** - * Compare two strings according to the sort order of the current language - * - * @param a - The first string to compare - * @param b - The second string to compare - */ - const compare = useCallback( - (a: string, b: string) => { - return collator.compare(a, b); - }, - [collator], - ); - - return { compare, set, getCountryName }; -} - -export default useLang; diff --git a/src/core/hooks/usePinnedItems.test.ts b/src/core/hooks/usePinnedItems.test.ts deleted file mode 100644 index 3741ffdb..00000000 --- a/src/core/hooks/usePinnedItems.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { act, renderHook } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { usePinnedItems } from "./usePinnedItems.ts"; - -const mockSetPinnedItems = vi.fn(); -const mockUseLocalStorage = vi.fn(); - -vi.mock("@core/hooks/useLocalStorage.ts", () => ({ - default: (...args) => mockUseLocalStorage(...args), -})); - -describe("usePinnedItems", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("returns default pinnedItems and togglePinnedItem", () => { - mockUseLocalStorage.mockReturnValue([[], mockSetPinnedItems]); - - const { result } = renderHook(() => - usePinnedItems({ storageName: "test-storage" }) - ); - - expect(result.current.pinnedItems).toEqual([]); - expect(typeof result.current.togglePinnedItem).toBe("function"); - }); - - it("adds an item if it's not already pinned", () => { - mockUseLocalStorage.mockReturnValue([["item1"], mockSetPinnedItems]); - - const { result } = renderHook(() => - usePinnedItems({ storageName: "test-storage" }) - ); - - act(() => { - result.current.togglePinnedItem("item2"); - }); - - expect(mockSetPinnedItems).toHaveBeenCalledWith(expect.any(Function)); - - const updater = mockSetPinnedItems.mock.calls[0][0]; - const updated = updater(["item1"]); - - expect(updated).toEqual(["item1", "item2"]); - }); - - it("removes an item if it's already pinned", () => { - mockUseLocalStorage.mockReturnValue([ - ["item1", "item2"], - mockSetPinnedItems, - ]); - - const { result } = renderHook(() => - usePinnedItems({ storageName: "test-storage" }) - ); - - act(() => { - result.current.togglePinnedItem("item1"); - }); - - expect(mockSetPinnedItems).toHaveBeenCalledWith(expect.any(Function)); - - const updater = mockSetPinnedItems.mock.calls[0][0]; - const updated = updater(["item1", "item2"]); - - expect(updated).toEqual(["item2"]); - }); -}); diff --git a/src/core/utils/test.tsx b/src/core/utils/test.tsx deleted file mode 100644 index cb9906ae..00000000 --- a/src/core/utils/test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - createMemoryHistory, - createRouter, - Outlet, - RootRoute, - Route, - RouterProvider, -} from "@tanstack/react-router"; -import { render as rtlRender, RenderOptions } from "@testing-library/react"; -import type { FunctionComponent, ReactElement, ReactNode } from "react"; - -// a root route for the test router. -const rootRoute = new RootRoute({ - component: () => ( - <> - - - ), -}); - -interface CustomRenderOptions extends Omit { - initialEntries?: string[]; - ui?: ReactElement; -} - -let currentRouter: ReturnType | null = null; - -/** - * Custom render function for testing components that need TanStack Router context. - * @param ui The main ReactElement to render (your component under test). - * @param options Custom render options including initialEntries for the router. - * @returns An object containing the testing-library render result and the router instance. - */ -const customRender = ( - ui: ReactElement, - options: CustomRenderOptions = {}, -) => { - const { initialEntries = ["/"], ...renderOptions } = options; - - // A specific route that renders the component under test (ui). - // It defaults to the first path in initialEntries or '/'. - const testComponentRoute = new Route({ - getParentRoute: () => rootRoute, - path: initialEntries[0] || "/", - component: () => ui, // The component passed to render will be the element for this route - }); - - const routeTree = rootRoute.addChildren([testComponentRoute]); - - const router = createRouter({ - history: createMemoryHistory({ initialEntries }), - routeTree, - // You can add default error components or other router options if needed for tests. - // defaultErrorComponent: ({ error }) =>
Test Error: {error.message}
, - }); - - currentRouter = router; // Store the router instance for access in tests - - const Wrapper: FunctionComponent<{ children?: ReactNode }> = ( - { children }, - ) => { - return ( - <> - - {children} - - ); - }; - - const renderResult = rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); - - return { - ...renderResult, - router, - }; -}; - -export * from "@testing-library/react"; -export { customRender as render }; -export const getTestRouter = () => currentRouter; diff --git a/src/i18n/config.ts b/src/i18n/config.ts deleted file mode 100644 index 4a43392f..00000000 --- a/src/i18n/config.ts +++ /dev/null @@ -1,52 +0,0 @@ -import i18next from "i18next"; -import { initReactI18next } from "react-i18next"; -import Backend from "i18next-http-backend"; -import LanguageDetector from "i18next-browser-languagedetector"; - -export type Lang = { code: string; name: string; flag: string }; -export type LangCode = Lang["code"]; - -export const supportedLanguages: Lang[] = [ - // { code: "de", name: "Deutsch", flag: "🇩🇪" }, - { code: "en", name: "English", flag: "🇺🇸" }, - // { code: "es", name: "Español", flag: "🇪🇸" }, - // { code: "fr", name: "Français", flag: "🇫🇷" }, - // { code: "zh", name: "中文", flag: "🇨🇳" }, -]; - -i18next - .use(Backend) - .use(initReactI18next) - .use(LanguageDetector) - .init({ - backend: { - // this will lazy load resources from the i18n folder - loadPath: "/src/i18n/locales/{{lng}}/{{ns}}.json", - }, - react: { - useSuspense: true, - }, - detection: { - order: ["navigator", "localStorage"], - }, - fallbackLng: { - "en-US": ["en"], - "en-CA": ["en-US", "en"], - "default": ["en"], - }, - fallbackNS: ["common", "ui", "dialog"], - debug: import.meta.env.DEV, - supportedLngs: supportedLanguages?.map((lang) => lang.code), - ns: [ - "channels", - "commandPalette", - "common", - "deviceConfig", - "moduleConfig", - "dashboard", - "dialog", - "messages", - "nodes", - "ui", - ], - }); diff --git a/src/i18n/locales/en/messages.json b/src/i18n/locales/en/messages.json deleted file mode 100644 index 4df0ac92..00000000 --- a/src/i18n/locales/en/messages.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "page": { - "title": "Messages: {{chatName}}" - }, - "emptyState": { - "title": "Select a Chat", - "text": "No messages yet." - }, - "selectChatPrompt": { - "text": "Select a channel or node to start messaging." - }, - "sendMessage": { - "placeholder": "Type your message here...", - "sendButton": "Send" - }, - "actionsMenu": { - "addReactionLabel": "Add Reaction", - "replyLabel": "Reply" - }, - - "item": { - "status": { - "delivered": { - "label": "Message delivered", - "displayText": "Message delivered" - }, - "failed": { - "label": "Message delivery failed", - "displayText": "Delivery failed" - }, - "unknown": { - "label": "Message status unknown", - "displayText": "Unknown state" - }, - "waiting": { - "ariaLabel": "Sending message", - "displayText": "Waiting for delivery" - } - } - } -} diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index dbaafd39..00000000 --- a/src/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import "@app/index.css"; -import { enableMapSet } from "immer"; -import "maplibre-gl/dist/maplibre-gl.css"; -import { StrictMode, Suspense } from "react"; -import { createRoot } from "react-dom/client"; -import "./i18n/config.ts"; -import { createRouter, RouterProvider } from "@tanstack/react-router"; -import { routeTree } from "@app/routes.tsx"; - -declare module "@tanstack/react-router" { - interface Register { - router: ReturnType; - } -} - -const container = document.getElementById("root") as HTMLElement; -const root = createRoot(container); - -enableMapSet(); - -const router = createRouter({ - routeTree, -}); - -root.render( - - - - - , -); diff --git a/src/pages/Config/DeviceConfig.tsx b/src/pages/Config/DeviceConfig.tsx deleted file mode 100644 index 8a42d0c4..00000000 --- a/src/pages/Config/DeviceConfig.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx"; -import { Device } from "@components/PageComponents/Config/Device/index.tsx"; -import { Display } from "@components/PageComponents/Config/Display.tsx"; -import { LoRa } from "@components/PageComponents/Config/LoRa.tsx"; -import { Network } from "@components/PageComponents/Config/Network/index.tsx"; -import { Position } from "@components/PageComponents/Config/Position.tsx"; -import { Power } from "@components/PageComponents/Config/Power.tsx"; -import { Security } from "@components/PageComponents/Config/Security/Security.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@components/UI/Tabs.tsx"; -import { useTranslation } from "react-i18next"; - -export const DeviceConfig = () => { - const { t } = useTranslation("deviceConfig"); - const tabs = [ - { - label: t("page.tabDevice"), - element: Device, - count: 0, - }, - { - label: t("page.tabPosition"), - element: Position, - }, - { - label: t("page.tabPower"), - element: Power, - }, - { - label: t("page.tabNetwork"), - element: Network, - }, - { - label: t("page.tabDisplay"), - element: Display, - }, - { - label: t("page.tabLora"), - element: LoRa, - }, - { - label: t("page.tabBluetooth"), - element: Bluetooth, - }, - { - label: t("page.tabSecurity"), - element: Security, - }, - ]; - - return ( - - - {tabs.map((tab) => ( - - {tab.label} - - ))} - - {tabs.map((tab) => ( - - - - ))} - - ); -}; diff --git a/src/pages/Config/index.tsx b/src/pages/Config/index.tsx deleted file mode 100644 index 337c2883..00000000 --- a/src/pages/Config/index.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useAppStore } from "../../core/stores/appStore.ts"; -import { useDevice } from "@core/stores/deviceStore.ts"; -import { PageLayout } from "@components/PageLayout.tsx"; -import { Sidebar } from "@components/Sidebar.tsx"; -import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; -import { SidebarButton } from "../../components/UI/Sidebar/SidebarButton.tsx"; -import { useToast } from "@core/hooks/useToast.ts"; -import { DeviceConfig } from "@pages/Config/DeviceConfig.tsx"; -import { ModuleConfig } from "@pages/Config/ModuleConfig.tsx"; -import { BoxesIcon, SaveIcon, SaveOff, SettingsIcon } from "lucide-react"; -import { useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; - -const ConfigPage = () => { - const { workingConfig, workingModuleConfig, connection } = useDevice(); - const { hasErrors } = useAppStore(); - const [activeConfigSection, setActiveConfigSection] = useState< - "device" | "module" - >("device"); - const [isSaving, setIsSaving] = useState(false); - const { toast } = useToast(); - const isError = hasErrors(); - const { t } = useTranslation("deviceConfig"); - - const handleSave = async () => { - if (hasErrors()) { - return toast({ - title: t("toast.validationError.title"), - description: t("toast.validationError.description"), - }); - } - - setIsSaving(true); - try { - if (activeConfigSection === "device") { - await Promise.all( - workingConfig.map((config) => - connection?.setConfig(config).then(() => - toast({ - title: t("toast.saveSuccess.title"), - description: t("toast.saveSuccess.description", { - case: config.payloadVariant.case, - }), - }) - ) - ), - ); - } else { - await Promise.all( - workingModuleConfig.map((moduleConfig) => - connection?.setModuleConfig(moduleConfig).then(() => - toast({ - title: t("toast.saveSuccess.title"), - description: t("toast.saveSuccess.description", { - case: moduleConfig.payloadVariant.case, - }), - }) - ) - ), - ); - setIsSaving(false); - } - await connection?.commitEditSettings(); - } catch (_error) { - toast({ - title: t("toast.configSaveError.title"), - description: t("toast.configSaveError.description"), - }); - } finally { - setIsSaving(false); - } - }; - - const leftSidebar = useMemo( - () => ( - - - setActiveConfigSection("device")} - Icon={SettingsIcon} - /> - setActiveConfigSection("module")} - Icon={BoxesIcon} - /> - - - ), - [activeConfigSection], - ); - - return ( - <> - - {activeConfigSection === "device" ? : } - - - ); -}; - -export default ConfigPage; diff --git a/src/pages/Messages.test.tsx b/src/pages/Messages.test.tsx deleted file mode 100644 index 46fc0115..00000000 --- a/src/pages/Messages.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { fireEvent, render, screen } from "@testing-library/react"; -import { MessagesPage } from "./Messages.tsx"; -import { useDevice } from "../core/stores/deviceStore.ts"; -import { Protobuf } from "@meshtastic/core"; - -vi.mock("../core/stores/deviceStore", () => ({ - useDevice: vi.fn(), -})); - -const mockUseDevice = { - channels: new Map([ - [0, { - index: 0, - settings: { name: "Primary" }, - role: Protobuf.Channel.Channel_Role.PRIMARY, - }], - ]), - nodes: new Map([ - [0, { - num: 0, - user: { longName: "Test Node 0", shortName: "TN0", publicKey: "0000" }, - }], - [1111, { - num: 1111, - user: { longName: "Test Node 1", shortName: "TN1", publicKey: "12345" }, - }], - [2222, { - num: 2222, - user: { longName: "Test Node 2", shortName: "TN2", publicKey: "67890" }, - }], - [3333, { - num: 3333, - user: { longName: "Test Node 3", shortName: "TN3", publicKey: "11111" }, - }], - ]), - hardware: { myNodeNum: 1 }, - messages: { broadcast: new Map(), direct: new Map() }, - metadata: new Map(), - unreadCounts: new Map([[1111, 3], [2222, 10]]), - resetUnread: vi.fn(), - hasNodeError: vi.fn(), -}; - -describe.skip("Messages Page", () => { - beforeEach(() => { - vi.mocked(useDevice).mockReturnValue(mockUseDevice); - }); - - it("sorts unreads to the top", () => { - render(); - const buttonOrder = screen.getAllByRole("button").filter((b) => - b.textContent.includes("Test Node") - ); - expect(buttonOrder[0].textContent).toContain("TN2Test Node 210"); - expect(buttonOrder[1].textContent).toContain("TN1Test Node 13"); - expect(buttonOrder[2].textContent).toContain("TN0Test Node 0"); - expect(buttonOrder[3].textContent).toContain("TN3Test Node 3"); - }); - - it("updates unread when active chat changes", () => { - render(); - const nodeButton = - screen.getAllByRole("button").filter((b) => - b.textContent.includes("TN1Test Node 13") - )[0]; - fireEvent.click(nodeButton); - expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0); - }); - - it("does not update the incorrect node", () => { - render(); - const nodeButton = - screen.getAllByRole("button").filter((b) => - b.textContent.includes("TN1Test Node 1") - )[0]; - fireEvent.click(nodeButton); - expect(mockUseDevice.resetUnread).toHaveBeenCalledWith(1111, 0); - expect(mockUseDevice.unreadCounts.get(2222)).toBe(10); - }); -}); diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts deleted file mode 100644 index 78c69447..00000000 --- a/src/routeTree.gen.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file was automatically generated by TanStack Router. -// You should NOT make any changes in this file as it will be overwritten. -// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. - -// Import Routes - -import { Route as rootRoute } from './routes/__root' - -// Create/Update Routes - -// Populate the FileRoutesByPath interface - -declare module '@tanstack/react-router' { - interface FileRoutesByPath {} -} - -// Create and export the route tree - -export interface FileRoutesByFullPath {} - -export interface FileRoutesByTo {} - -export interface FileRoutesById { - __root__: typeof rootRoute -} - -export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: never - fileRoutesByTo: FileRoutesByTo - to: never - id: '__root__' - fileRoutesById: FileRoutesById -} - -export interface RootRouteChildren {} - -const rootRouteChildren: RootRouteChildren = {} - -export const routeTree = rootRoute - ._addFileChildren(rootRouteChildren) - ._addFileTypes() - -/* ROUTE_MANIFEST_START -{ - "routes": { - "__root__": { - "filePath": "__root.tsx", - "children": [] - } - } -} -ROUTE_MANIFEST_END */ diff --git a/src/routes.tsx b/src/routes.tsx deleted file mode 100644 index 2683301a..00000000 --- a/src/routes.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { createRoute } from "@tanstack/react-router"; -import { Dashboard } from "@pages/Dashboard/index.tsx"; -import MessagesPage from "@pages/Messages.tsx"; -import MapPage from "@pages/Map/index.tsx"; -import ConfigPage from "@pages/Config/index.tsx"; -import ChannelsPage from "@pages/Channels.tsx"; -import NodesPage from "@pages/Nodes.tsx"; -import { createRootRoute } from "@tanstack/react-router"; -import { App } from "./App.tsx"; -import { DialogManager } from "@components/Dialog/DialogManager.tsx"; - -const rootRoute = createRootRoute({ - component: App, -}); - -const indexRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/", - component: Dashboard, -}); - -const messagesRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/messages", - component: MessagesPage, -}); - -const messagesWithParamsRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/messages/$type/$chatId", - component: MessagesPage, -}); - -const mapRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/map", - component: MapPage, -}); - -const configRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/config", - component: ConfigPage, -}); - -const channelsRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/channels", - component: ChannelsPage, -}); - -const nodesRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/nodes", - component: NodesPage, -}); - -const dialogWithParamsRoute = createRoute({ - getParentRoute: () => rootRoute, - path: "/dialog/$dialogId", - component: DialogManager, -}); - -export const routeTree = rootRoute.addChildren([ - indexRoute, - messagesRoute, - messagesWithParamsRoute, - mapRoute, - configRoute, - channelsRoute, - nodesRoute, - dialogWithParamsRoute, -]); - -export { rootRoute };