mirror of
https://github.com/Lissy93/dashy.git
synced 2026-06-01 22:34:44 -04:00
Merge remote-tracking branch 'origin/master' into dependabot/npm_and_yarn/sentry/vue-10.53.1
Co-authored-by: lissy93 <1862727+lissy93@users.noreply.github.com>
This commit is contained in:
3
.env
3
.env
@@ -64,6 +64,9 @@
|
||||
# Directory for conf.yml backups
|
||||
# BACKUP_DIR=./user-data/config-backups
|
||||
|
||||
# Set to 'true' to disable automatic backups before each config save
|
||||
# DISABLE_CONFIG_BACKUPS=true
|
||||
|
||||
# Setup any other user defined vars by prepending VITE_APP_ to the var name
|
||||
# VITE_APP_pihole_ip=http://your.pihole.ip
|
||||
# VITE_APP_pihole_key=your_pihole_secret_key
|
||||
|
||||
87
.github/workflows/build-release-assets.yml
vendored
87
.github/workflows/build-release-assets.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: 📦 Build & Upload Release Assets
|
||||
|
||||
# Builds Dashy and uploads a pre-built tarball to the GitHub release.
|
||||
# This allows non-Docker installs (e.g. Proxmox VE community scripts) to
|
||||
# download a ready-to-run package without having to build from source.
|
||||
#
|
||||
# The tarball contains the compiled frontend (dist/) plus all server-side
|
||||
# files. Users extract it and run `yarn install --production` + `node server`.
|
||||
#
|
||||
# Triggered whenever a new release is created, or when manually dispatched
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to build assets for (must already exist as a release)'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.release.tag_name || github.event.inputs.tag }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-release-assets:
|
||||
name: Build app & upload tarball
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG: ${{ github.event.release.tag_name || github.event.inputs.tag }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code 🛎️
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/${{ env.TAG }}
|
||||
|
||||
- name: Setup Node.js ⚙️
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies 📥
|
||||
run: yarn install --frozen-lockfile --ignore-engines --network-timeout 300000
|
||||
|
||||
- name: Build app 🏗️
|
||||
run: NODE_OPTIONS=--openssl-legacy-provider yarn build --mode production
|
||||
|
||||
- name: Package release artifact 📦
|
||||
run: |
|
||||
STAGING="dashy-release-staging"
|
||||
mkdir -p "$STAGING"
|
||||
|
||||
# Runtime files
|
||||
cp -r dist "$STAGING/"
|
||||
cp -r services "$STAGING/"
|
||||
cp -r public "$STAGING/"
|
||||
cp -r user-data "$STAGING/"
|
||||
cp server.js "$STAGING/"
|
||||
cp yarn.lock "$STAGING/"
|
||||
|
||||
# src/utils/ files referenced directly by the server at runtime
|
||||
mkdir -p "$STAGING/src/utils/config"
|
||||
cp src/utils/config/ConfigSchema.json "$STAGING/src/utils/config/"
|
||||
|
||||
# Strip devDependencies so `yarn install --production` stays lean
|
||||
node -e "
|
||||
const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8'));
|
||||
delete pkg.devDependencies;
|
||||
require('fs').writeFileSync('$STAGING/package.json', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
|
||||
TARBALL="dashy-${TAG}.tar.gz"
|
||||
tar -czf "${TARBALL}" -C "${STAGING}" .
|
||||
echo "TARBALL=${TARBALL}" >> "$GITHUB_ENV"
|
||||
echo "Size: $(du -sh ${TARBALL} | cut -f1)"
|
||||
|
||||
- name: Upload tarball to GitHub Release 🚀
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
files: ${{ env.TARBALL }}
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
240
.github/workflows/ci.yml
vendored
Normal file
240
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
# CI checks to run when PR is opened
|
||||
name: 🚦 PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['master', 'develop']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: 🔎 Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
lockfile: ${{ steps.filter.outputs.lockfile }}
|
||||
workflows: ${{ steps.filter.outputs.workflows }}
|
||||
locales: ${{ steps.filter.outputs.locales }}
|
||||
translations: ${{ steps.filter.outputs.translations }}
|
||||
src: ${{ steps.filter.outputs.src }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Filter Paths
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
lockfile:
|
||||
- 'yarn.lock'
|
||||
workflows:
|
||||
- '.github/workflows/**'
|
||||
locales:
|
||||
- 'src/assets/locales/**'
|
||||
- 'src/**/*.vue'
|
||||
- 'src/**/*.js'
|
||||
- 'tests/locales/**'
|
||||
translations:
|
||||
- 'src/assets/locales/**'
|
||||
src:
|
||||
- 'src/**'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'eslint.config.mjs'
|
||||
- 'tsconfig.json'
|
||||
|
||||
lint:
|
||||
name: 🛡️ Lint
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.src == 'true'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Run ESLint
|
||||
run: yarn lint
|
||||
|
||||
typecheck:
|
||||
name: 🦴 Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.src == 'true'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Run vue-tsc
|
||||
run: yarn typecheck
|
||||
|
||||
test:
|
||||
name: 🧪 Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Run Tests
|
||||
run: yarn test
|
||||
|
||||
locales:
|
||||
name: 🌐 Locale Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.locales == 'true'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Check Locales
|
||||
run: yarn validate-locales
|
||||
|
||||
spellcheck:
|
||||
name: ✏️ Spellcheck
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.translations == 'true'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Spellcheck en.json
|
||||
uses: crate-ci/typos@v1
|
||||
with:
|
||||
files: src/assets/locales/en.json
|
||||
|
||||
build:
|
||||
name: 🏗️ Build Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build Project
|
||||
run: yarn build
|
||||
|
||||
- name: Verify Build Output
|
||||
run: |
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "❌ Build failed: dist directory not created"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "dist/index.html" ]; then
|
||||
echo "❌ Build failed: index.html not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Build successful"
|
||||
|
||||
docker-smoke:
|
||||
name: 🐳 Docker Smoke Test
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build & Test Docker Image
|
||||
run: sh tests/docker-smoke-test.sh
|
||||
timeout-minutes: 10
|
||||
|
||||
dependency-review:
|
||||
name: 🔒 Dependency Audit
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.lockfile == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Review Dependencies
|
||||
uses: actions/dependency-review-action@v5
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
|
||||
secret-scan:
|
||||
name: 🔑 Secret Scanning
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan PR Diff for Secrets
|
||||
uses: trufflesecurity/trufflehog@v3.95.3
|
||||
with:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
extra_args: --only-verified
|
||||
|
||||
workflow-audit:
|
||||
name: 🛠️ Workflow Audit
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.workflows == 'true'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run Actionlint
|
||||
uses: raven-actions/actionlint@v2
|
||||
with:
|
||||
fail-on-error: true
|
||||
|
||||
- name: Run Zizmor
|
||||
uses: zizmorcore/zizmor-action@v0.5.4
|
||||
with:
|
||||
inputs: .github/workflows/
|
||||
advanced-security: false
|
||||
annotations: true
|
||||
84
.github/workflows/docker-build-publish.yml
vendored
84
.github/workflows/docker-build-publish.yml
vendored
@@ -1,84 +0,0 @@
|
||||
name: 🐳 Build + Publish Multi-Platform Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['*.*.*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
env:
|
||||
DH_IMAGE: ${{ secrets.DOCKER_REPO }}
|
||||
GH_IMAGE: ${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: { contents: read, packages: write }
|
||||
|
||||
steps:
|
||||
- name: 🛎️ Checkout Repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🗂️ Make Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ env.DH_IMAGE }}
|
||||
ghcr.io/${{ env.GH_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}.x
|
||||
type=raw,value=latest
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: ⏱️ Capture Build Timestamp
|
||||
id: timestamp
|
||||
run: echo "iso=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: 🔑 Login to DockerHub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: 🔑 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🚦 Check Registry Status
|
||||
uses: crazy-max/ghaction-docker-status@v4
|
||||
|
||||
- name: ⚒️ Build and Push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
REVISION=${{ github.sha }}
|
||||
CREATED=${{ steps.timestamp.outputs.iso }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
push: true
|
||||
400
.github/workflows/docker.yml
vendored
Normal file
400
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,400 @@
|
||||
# Builds and publishes the multi-arch Docker image
|
||||
#
|
||||
# Triggered by:
|
||||
# - On git tag push, publishes to :X.Y.Z, :X.Y, and :latest
|
||||
# - On manual dispatch from master, rebuilds and updates :latest
|
||||
# - On weekly cron, rebuilds :latest from master for upstream patches
|
||||
#
|
||||
# The workflow will:
|
||||
# - Builds multi-arch (amd64, arm64, armv7) in parallel on native runners
|
||||
# - Trivy scans + reports security issues, and fails on CRITICAL CVEs
|
||||
# - Publishes to GHCR, and to Docker Hub if creds are configured
|
||||
# - Attests both the build provenance and SBOM and publishes to GHCR
|
||||
# - Uploads digest, SBOM and outputs as artifact, and shows MD summary
|
||||
|
||||
name: 🐳 Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing git tag to build. Empty = build current ref as :latest.'
|
||||
required: false
|
||||
default: ''
|
||||
push:
|
||||
tags: ['*.*.*']
|
||||
schedule:
|
||||
- cron: '0 4 * * 0'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DH_IMAGE: ${{ vars.DOCKER_REPO || 'lissy93/dashy' }}
|
||||
GH_IMAGE: ghcr.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 🔨 Build (${{ matrix.arch }})
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: "false"
|
||||
DOCKER_BUILD_RECORD_UPLOAD: "false"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
- platform: linux/arm/v7
|
||||
runner: ubuntu-latest
|
||||
arch: armv7
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
|
||||
- name: 🏷️ Resolve build version
|
||||
id: version
|
||||
env:
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$INPUT_TAG" ]; then
|
||||
v="$INPUT_TAG"
|
||||
elif [ "$EVENT_NAME" = "push" ]; then
|
||||
v="$REF_NAME"
|
||||
else
|
||||
v="latest"
|
||||
fi
|
||||
echo "value=$v" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔧 Set up QEMU
|
||||
if: matrix.arch == 'armv7'
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: linux/arm/v7
|
||||
|
||||
- name: 🔧 Set up Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: 🔑 Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: ⏱️ Capture build timestamp
|
||||
id: timestamp
|
||||
run: echo "iso=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔨 Build image (load for scan)
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,scope=${{ matrix.arch }},mode=max
|
||||
load: true
|
||||
tags: dashy-scan:${{ matrix.arch }}
|
||||
provenance: false
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.value }}
|
||||
REVISION=${{ github.sha }}
|
||||
CREATED=${{ steps.timestamp.outputs.iso }}
|
||||
|
||||
- name: 🛡️ Trivy vulnerability scan
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
env:
|
||||
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
|
||||
TRIVY_JAVA_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-java-db:1
|
||||
with:
|
||||
version: v0.70.0
|
||||
image-ref: dashy-scan:${{ matrix.arch }}
|
||||
severity: CRITICAL
|
||||
ignore-unfixed: true
|
||||
exit-code: ${{ github.event_name == 'schedule' && '1' || '0' }}
|
||||
vuln-type: 'os,library'
|
||||
format: 'sarif'
|
||||
output: 'trivy-${{ matrix.arch }}.sarif'
|
||||
timeout: '10m'
|
||||
|
||||
- name: 📤 Upload Trivy SARIF
|
||||
if: always() && hashFiles(format('trivy-{0}.sarif', matrix.arch)) != ''
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: trivy-${{ matrix.arch }}.sarif
|
||||
category: trivy-${{ matrix.arch }}
|
||||
|
||||
- name: 📤 Upload Trivy artifact
|
||||
if: always() && hashFiles(format('trivy-{0}.sarif', matrix.arch)) != ''
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: trivy-${{ matrix.arch }}
|
||||
path: trivy-${{ matrix.arch }}.sarif
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: 🚀 Push by digest
|
||||
id: push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
outputs: type=image,name=${{ env.GH_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
provenance: false
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.value }}
|
||||
REVISION=${{ github.sha }}
|
||||
CREATED=${{ steps.timestamp.outputs.iso }}
|
||||
|
||||
- name: 🧬 Write digest
|
||||
env:
|
||||
DIGEST: ${{ steps.push.outputs.digest }}
|
||||
DIGESTS_DIR: ${{ runner.temp }}/digests
|
||||
ARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
mkdir -p "$DIGESTS_DIR"
|
||||
echo "$DIGEST" > "$DIGESTS_DIR/$ARCH"
|
||||
|
||||
- name: 📤 Upload digest
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digest-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/digests/${{ matrix.arch }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: 🧩 Merge & Push Manifests
|
||||
needs: build
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
env:
|
||||
HAS_DH: ${{ secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }}
|
||||
SEMVER_VALUE: ${{ inputs.tag || github.ref_name }}
|
||||
SEMVER_ENABLE: ${{ github.event_name == 'push' || inputs.tag != '' }}
|
||||
LATEST_ENABLE: ${{ inputs.tag == '' }}
|
||||
steps:
|
||||
- name: 📥 Download digests
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digest-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: 📥 Download Trivy SARIFs
|
||||
uses: actions/download-artifact@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ${{ runner.temp }}/trivy
|
||||
pattern: trivy-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: 🔧 Set up Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: 🔑 Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🔑 Login to Docker Hub
|
||||
if: env.HAS_DH == 'true'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: 🗂️ Generate tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ env.GH_IMAGE }}
|
||||
${{ env.HAS_DH == 'true' && env.DH_IMAGE || '' }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ env.LATEST_ENABLE }}
|
||||
type=semver,pattern={{version}},value=${{ env.SEMVER_VALUE }},enable=${{ env.SEMVER_ENABLE }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ env.SEMVER_VALUE }},enable=${{ env.SEMVER_ENABLE }}
|
||||
type=semver,pattern={{major}}.x,value=${{ env.SEMVER_VALUE }},enable=${{ env.SEMVER_ENABLE }}
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: 🧩 Create & push manifest
|
||||
id: manifest
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAGS=()
|
||||
while IFS= read -r tag; do TAGS+=(-t "$tag"); done \
|
||||
< <(jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||
SOURCES=()
|
||||
for f in *; do SOURCES+=("${GH_IMAGE}@$(cat "$f")"); done
|
||||
docker buildx imagetools create "${TAGS[@]}" "${SOURCES[@]}"
|
||||
PRIMARY=$(jq -r --arg img "$GH_IMAGE" \
|
||||
'[.tags[] | select(startswith($img + ":"))] | first // empty' \
|
||||
<<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||
DIGEST=$(docker buildx imagetools inspect "$PRIMARY" --format '{{.Manifest.Digest}}')
|
||||
echo "primary_tag=$PRIMARY" >> "$GITHUB_OUTPUT"
|
||||
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔐 Generate SBOM (SPDX)
|
||||
uses: anchore/sbom-action@v0.24.0
|
||||
with:
|
||||
image: ${{ steps.manifest.outputs.primary_tag }}
|
||||
format: spdx-json
|
||||
output-file: sbom.spdx.json
|
||||
upload-artifact: false
|
||||
|
||||
- name: 🪪 Attest SBOM
|
||||
id: attest_sbom
|
||||
uses: actions/attest@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
subject-name: ${{ env.GH_IMAGE }}
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
sbom-path: sbom.spdx.json
|
||||
push-to-registry: true
|
||||
|
||||
- name: 🛡️ Attest build provenance
|
||||
id: attest_provenance
|
||||
uses: actions/attest-build-provenance@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
subject-name: ${{ env.GH_IMAGE }}
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: 📋 Summary
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
SBOM_OUTCOME: ${{ steps.attest_sbom.outcome }}
|
||||
SBOM_URL: ${{ steps.attest_sbom.outputs.attestation-url }}
|
||||
PROV_OUTCOME: ${{ steps.attest_provenance.outcome }}
|
||||
PROV_URL: ${{ steps.attest_provenance.outputs.attestation-url }}
|
||||
DIGEST: ${{ steps.manifest.outputs.digest }}
|
||||
PRIMARY: ${{ steps.manifest.outputs.primary_tag }}
|
||||
TAGS_JSON: ${{ steps.meta.outputs.json }}
|
||||
DIGESTS_DIR: ${{ runner.temp }}/digests
|
||||
TRIVY_DIR: ${{ runner.temp }}/trivy
|
||||
# Behold, some ugly bash, to produce a pretty output (don't read it, just trust)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
attest_line() {
|
||||
local label="$1" outcome="$2" url="$3"
|
||||
case "$outcome" in
|
||||
success)
|
||||
if [ -n "$url" ]; then
|
||||
echo "- ✅ $label attested ([view]($url))"
|
||||
else
|
||||
echo "- ✅ $label attested"
|
||||
fi ;;
|
||||
failure) echo "- ⚠️ $label attestation failed (image pushed without attest)" ;;
|
||||
*) echo "- ⏭️ $label attestation \`$outcome\`" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
trivy_section() {
|
||||
local dir="$1"
|
||||
[ -d "$dir" ] || return 0
|
||||
local found=0
|
||||
local lines=""
|
||||
local arch f n
|
||||
for arch in amd64 arm64 armv7; do
|
||||
f="$dir/trivy-$arch.sarif"
|
||||
[ -f "$f" ] || continue
|
||||
found=1
|
||||
n=$(jq '[.runs[]?.results[]?] | length' "$f" 2>/dev/null || echo 0)
|
||||
[[ "$n" =~ ^[0-9]+$ ]] || n=0
|
||||
if [ "$n" = "0" ]; then
|
||||
lines+="- ✅ \`$arch\` — no fixable CRITICAL CVEs"$'\n'
|
||||
else
|
||||
lines+="- ⚠️ \`$arch\` — $n fixable CRITICAL CVE(s)"$'\n'
|
||||
fi
|
||||
done
|
||||
[ "$found" = "1" ] || return 0
|
||||
echo "## Security Scan"
|
||||
echo ""
|
||||
echo "Trivy (CRITICAL severity, fixable only):"
|
||||
echo ""
|
||||
printf '%s\n' "$lines"
|
||||
echo "---"
|
||||
echo ""
|
||||
}
|
||||
|
||||
arch_section() {
|
||||
local arch="$1"
|
||||
local file="$DIGESTS_DIR/$arch"
|
||||
[ -f "$file" ] || return 0
|
||||
local digest manifest size count
|
||||
digest=$(cat "$file")
|
||||
manifest=$(docker buildx imagetools inspect "${PRIMARY%%:*}@$digest" --raw 2>/dev/null || echo '{}')
|
||||
size=$(jq '[.layers[]?.size // 0] | add // 0' <<< "$manifest")
|
||||
count=$(jq '.layers // [] | length' <<< "$manifest")
|
||||
echo "#### Dashy \`$arch\`"
|
||||
echo ""
|
||||
echo "- **Digest:** \`$digest\`"
|
||||
[ "$size" != "0" ] && echo "- **Size:** $(numfmt --to=iec --suffix=B "$size" 2>/dev/null || echo "$size B")"
|
||||
[ "$count" != "0" ] && echo "- **Layers:** $count"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Clear auto-generated "Attestation Created" blocks from attest actions.
|
||||
: > "$GITHUB_STEP_SUMMARY"
|
||||
{
|
||||
if [ -n "$DIGEST" ]; then
|
||||
echo "## Docker Image"
|
||||
echo ""
|
||||
echo "**Manifest:** \`$DIGEST\`"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
jq -r '.tags[] | "docker pull \(.)"' <<< "$TAGS_JSON"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
fi
|
||||
echo "## Attestations"
|
||||
echo ""
|
||||
attest_line "SBOM" "$SBOM_OUTCOME" "$SBOM_URL"
|
||||
attest_line "Build provenance" "$PROV_OUTCOME" "$PROV_URL"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
trivy_section "$TRIVY_DIR"
|
||||
echo "## Build Info"
|
||||
echo ""
|
||||
for arch in amd64 arm64 armv7; do
|
||||
arch_section "$arch"
|
||||
done
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
103
.github/workflows/draft-release.yml
vendored
103
.github/workflows/draft-release.yml
vendored
@@ -1,103 +0,0 @@
|
||||
name: 🏗️ Draft New Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to draft a release for (must already exist)'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-draft-release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout code 🛎️
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if major or minor version changed 🔍
|
||||
id: version_check
|
||||
env:
|
||||
CURRENT_TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
git fetch --tags --force
|
||||
CURRENT_MM=$(echo "$CURRENT_TAG" | sed 's/^v//; s/\([0-9]*\.[0-9]*\)\..*/\1/')
|
||||
|
||||
# Find the immediately previous tag (to detect patch-only bumps)
|
||||
PREVIOUS_TAG=$(git tag --sort=-version:refname \
|
||||
| grep -v "^${CURRENT_TAG}$" | head -1)
|
||||
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
echo "No previous tag found, creating release"
|
||||
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||
echo "previous_tag=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PREVIOUS_MM=$(echo "$PREVIOUS_TAG" | sed 's/^v//; s/\([0-9]*\.[0-9]*\)\..*/\1/')
|
||||
if [ "$CURRENT_MM" = "$PREVIOUS_MM" ]; then
|
||||
echo "Patch-only bump ($PREVIOUS_TAG -> $CURRENT_TAG), skipping"
|
||||
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||
echo "previous_tag=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Minor/major bump — find the last tag from the previous release
|
||||
PREV_RELEASE_TAG=$(git tag --sort=-version:refname | while read -r t; do
|
||||
[ "$t" = "$CURRENT_TAG" ] && continue
|
||||
t_mm=$(echo "$t" | sed 's/^v//; s/\([0-9]*\.[0-9]*\)\..*/\1/')
|
||||
if [ "$t_mm" != "$CURRENT_MM" ]; then echo "$t"; break; fi
|
||||
done)
|
||||
echo "Minor/major bump, comparing against ${PREV_RELEASE_TAG:-$PREVIOUS_TAG}"
|
||||
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||
echo "previous_tag=${PREV_RELEASE_TAG:-$PREVIOUS_TAG}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create draft release 📝
|
||||
if: steps.version_check.outputs.should_release == 'true' || github.event_name == 'workflow_dispatch'
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
name: Release ${{ env.TAG }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
previous_tag: ${{ steps.version_check.outputs.previous_tag }}
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Job summary 📋
|
||||
if: always()
|
||||
env:
|
||||
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||
SHOULD_RELEASE: ${{ steps.version_check.outputs.should_release }}
|
||||
RELEASE_URL: ${{ steps.create_release.outputs.url }}
|
||||
PREV_TAG: ${{ steps.version_check.outputs.previous_tag }}
|
||||
run: |
|
||||
{
|
||||
echo "## 🏗️ Draft Release"
|
||||
echo ""
|
||||
echo "| Step | Result |"
|
||||
echo "|------|--------|"
|
||||
echo "| Tag | [\`${TAG}\`](${REPO_URL}/releases/tag/${TAG}) |"
|
||||
|
||||
if [ -n "$PREV_TAG" ]; then
|
||||
echo "| Compared against | [\`${PREV_TAG}\`](${REPO_URL}/releases/tag/${PREV_TAG}) |"
|
||||
fi
|
||||
|
||||
if [ -n "$RELEASE_URL" ]; then
|
||||
echo "| Draft release | ✅ [Review and publish](${RELEASE_URL}) |"
|
||||
elif [ "$SHOULD_RELEASE" = "false" ]; then
|
||||
echo "| Draft release | ⏭️ Skipped (patch-only bump) |"
|
||||
else
|
||||
echo "| Draft release | ❌ Failed |"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
66
.github/workflows/manual-tag.yml
vendored
66
.github/workflows/manual-tag.yml
vendored
@@ -1,66 +0,0 @@
|
||||
# Manual fallback for creating a tag with optional version bump.
|
||||
# The automated flow is handled by bump-and-tag.yml on PR merge.
|
||||
name: 🏷️ Tag on Version Change
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to tag (e.g. 3.2.0). Leave empty to auto-bump patch.'
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: manual-tag-version
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
tag-version:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Repository 🛎️
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git Identity 🤖
|
||||
run: |
|
||||
git config user.name "Liss-Bot"
|
||||
git config user.email "liss-bot@d0h.co"
|
||||
|
||||
- name: Determine and Apply Version 🔢
|
||||
id: version
|
||||
env:
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
CURRENT=$(node -p "require('./package.json').version")
|
||||
if [ -n "$INPUT_VERSION" ]; then
|
||||
TARGET="${INPUT_VERSION#v}"
|
||||
else
|
||||
npm version patch --no-git-tag-version > /dev/null
|
||||
TARGET=$(node -p "require('./package.json').version")
|
||||
fi
|
||||
if [ "$TARGET" != "$CURRENT" ]; then
|
||||
npm version "$TARGET" --no-git-tag-version --allow-same-version
|
||||
git add package.json
|
||||
git commit -m "🔖 Bump version to $TARGET [skip ci]"
|
||||
git push
|
||||
echo "Committed version bump to $TARGET"
|
||||
else
|
||||
echo "package.json already at $CURRENT, skipping commit"
|
||||
fi
|
||||
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and Push Tag ⤴️
|
||||
env:
|
||||
TAG: ${{ steps.version.outputs.TARGET }}
|
||||
run: |
|
||||
git fetch --tags --force
|
||||
if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG already exists, skipping"
|
||||
else
|
||||
git tag -a "$TAG" -m "Release v$TAG"
|
||||
git push origin "$TAG"
|
||||
echo "Created and pushed tag $TAG"
|
||||
fi
|
||||
6
.github/workflows/mirror.yml
vendored
6
.github/workflows/mirror.yml
vendored
@@ -1,11 +1,15 @@
|
||||
# Syncs the full source of the Dashy repo over to our Codeberg mirror
|
||||
# For all you non-Microsoft babes!
|
||||
# This is then accessible over at https://codeberg.org/alicia/dashy
|
||||
name: 🪞 Mirror to Codeberg
|
||||
name: 🪞 Mirror
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 3 * * 0' # At 03:30 on Sunday
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codeberg:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
151
.github/workflows/pr-quality-check.yml
vendored
151
.github/workflows/pr-quality-check.yml
vendored
@@ -1,151 +0,0 @@
|
||||
name: 🔍 PR Quality Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['master', 'develop']
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: 🔎 Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
lockfile: ${{ steps.filter.outputs.lockfile }}
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔎 Filter Paths
|
||||
uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
lockfile:
|
||||
- 'yarn.lock'
|
||||
|
||||
lint:
|
||||
name: 📝 Lint Code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔧 Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🔍 Run ESLint
|
||||
run: yarn lint
|
||||
|
||||
typecheck:
|
||||
name: 🧷 Type Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔧 Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🧷 Run vue-tsc
|
||||
run: yarn typecheck
|
||||
|
||||
test:
|
||||
name: 🧪 Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔧 Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🧪 Run Tests
|
||||
run: yarn test
|
||||
|
||||
build:
|
||||
name: 🏗️ Build Application
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔧 Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🏗️ Build Project
|
||||
run: yarn build
|
||||
|
||||
- name: ✅ Verify Build Output
|
||||
run: |
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "❌ Build failed: dist directory not created"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "dist/index.html" ]; then
|
||||
echo "❌ Build failed: index.html not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Build successful"
|
||||
|
||||
docker-smoke:
|
||||
name: 🐳 Docker Smoke Test
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🐳 Build & Test Docker Image
|
||||
run: sh tests/docker-smoke-test.sh
|
||||
timeout-minutes: 10
|
||||
|
||||
security:
|
||||
name: 🔒 Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.lockfile == 'true'
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔧 Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🔒 Run Security Audit
|
||||
run: yarn audit --level high
|
||||
171
.github/workflows/release.yml
vendored
Normal file
171
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
# Builds Dashy and drafts a GitHub release with the compiled tarball,
|
||||
# along with SHA256 checksum and SLSA build-provenance attestation
|
||||
#
|
||||
# Triggered by:
|
||||
# - Push of any major/minor (X.Y.0) git tag
|
||||
# - Manual dispatch with any existing tag (any version)
|
||||
|
||||
name: 🚀 Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['*.*.0']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing git tag to release (e.g. 4.2.0)'
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.tag || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: 🚀 Build & Draft Release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
env:
|
||||
TAG: ${{ inputs.tag || github.ref_name }}
|
||||
steps:
|
||||
- name: 🛎️ Checkout tag
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/${{ env.TAG }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔧 Setup Node.js
|
||||
uses: actions/setup-node@v6 # zizmor: ignore[cache-poisoning]
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: yarn install --frozen-lockfile --ignore-engines --network-timeout 300000
|
||||
|
||||
- name: 🏗️ Build app
|
||||
run: NODE_OPTIONS=--openssl-legacy-provider yarn build --mode production
|
||||
|
||||
- name: 📦 Package release tarball
|
||||
id: package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
STAGING="dashy-release-staging"
|
||||
mkdir -p "$STAGING"
|
||||
cp -r dist "$STAGING/"
|
||||
cp -r services "$STAGING/"
|
||||
cp -r public "$STAGING/"
|
||||
cp -r user-data "$STAGING/"
|
||||
cp server.js "$STAGING/"
|
||||
cp yarn.lock "$STAGING/"
|
||||
mkdir -p "$STAGING/src/utils/config"
|
||||
cp src/utils/config/ConfigSchema.json "$STAGING/src/utils/config/"
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
delete pkg.devDependencies;
|
||||
fs.writeFileSync('$STAGING/package.json', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
TARBALL="dashy-${TAG}.tar.gz"
|
||||
tar -czf "$TARBALL" -C "$STAGING" .
|
||||
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
|
||||
echo "size=$(du -h "$TARBALL" | cut -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔢 Generate SHA256 checksum
|
||||
id: checksum
|
||||
env:
|
||||
TARBALL: ${{ steps.package.outputs.tarball }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CHECKSUM="${TARBALL}.sha256"
|
||||
sha256sum "$TARBALL" > "$CHECKSUM"
|
||||
echo "file=$CHECKSUM" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🪪 Generate build provenance attestation
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: ${{ steps.package.outputs.tarball }}
|
||||
|
||||
- name: 📤 Rename attestation bundle
|
||||
id: attest_asset
|
||||
env:
|
||||
TARBALL: ${{ steps.package.outputs.tarball }}
|
||||
BUNDLE: ${{ steps.attest.outputs.bundle-path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
OUT="${TARBALL}.intoto.jsonl"
|
||||
cp "$BUNDLE" "$OUT"
|
||||
echo "file=$OUT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔎 Find previous release tag
|
||||
id: prev
|
||||
env:
|
||||
CURRENT_TAG: ${{ env.TAG }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --tags --force
|
||||
PREV=$({ echo "$CURRENT_TAG"; git tag | grep -E '^[0-9]+\.[0-9]+\.0$' || true; } \
|
||||
| sort -uV \
|
||||
| awk -v cur="$CURRENT_TAG" '$0 == cur { print prev; exit } { prev = $0 }')
|
||||
echo "tag=$PREV" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 📝 Create draft release
|
||||
id: release
|
||||
# Kept over `gh release create` for built-in generate_release_notes,
|
||||
# previous_tag selection, fail_on_unmatched_files, and multi-file upload.
|
||||
uses: softprops/action-gh-release@v3 # zizmor: ignore[superfluous-actions]
|
||||
with:
|
||||
tag_name: ${{ env.TAG }}
|
||||
name: Release ${{ env.TAG }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
previous_tag: ${{ steps.prev.outputs.tag }}
|
||||
fail_on_unmatched_files: true
|
||||
files: |
|
||||
${{ steps.package.outputs.tarball }}
|
||||
${{ steps.checksum.outputs.file }}
|
||||
${{ steps.attest_asset.outputs.file }}
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📋 Job summary
|
||||
if: always()
|
||||
env:
|
||||
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||
PREV_TAG: ${{ steps.prev.outputs.tag }}
|
||||
RELEASE_URL: ${{ steps.release.outputs.url }}
|
||||
TARBALL: ${{ steps.package.outputs.tarball }}
|
||||
SIZE: ${{ steps.package.outputs.size }}
|
||||
ATTEST_URL: ${{ steps.attest.outputs.attestation-url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo "## 🚀 Release Draft"
|
||||
echo ""
|
||||
echo "| Item | Value |"
|
||||
echo "|------|-------|"
|
||||
echo "| Tag | [\`${TAG}\`](${REPO_URL}/releases/tag/${TAG}) |"
|
||||
if [ -n "$PREV_TAG" ]; then
|
||||
echo "| Notes since | [\`${PREV_TAG}\`](${REPO_URL}/releases/tag/${PREV_TAG}) |"
|
||||
fi
|
||||
if [ -n "$TARBALL" ]; then
|
||||
echo "| Tarball | \`${TARBALL}\` (${SIZE:-?}) |"
|
||||
fi
|
||||
if [ -n "$ATTEST_URL" ]; then
|
||||
echo "| Attestation | ✅ [View](${ATTEST_URL}) |"
|
||||
else
|
||||
echo "| Attestation | ❌ Failed |"
|
||||
fi
|
||||
if [ -n "$RELEASE_URL" ]; then
|
||||
echo "| Draft release | ✅ [Review and publish](${RELEASE_URL}) |"
|
||||
else
|
||||
echo "| Draft release | ❌ Failed |"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -1,18 +1,32 @@
|
||||
# Creates a new git tag when a PR is merged
|
||||
# Creates a new git tag when a PR is merged, or on manual dispatch
|
||||
#
|
||||
# Here's the flow:
|
||||
# PR trigger flow:
|
||||
# - Triggered whenever a PR is merged, if that PR made code changes
|
||||
# - If version wasn't bumped in PR, increment patch version and update package.json
|
||||
# - Otherwise (if the PR did bump version) we use the new version from package.json
|
||||
# - Creates and pushes a git tag for the new version
|
||||
# - That git tag then triggers Docker publishing and release drafting in other CI
|
||||
# - Add tags to issues from newly relesaed features/fixes (if applicable)
|
||||
# - Add labels and release comments to referenced issues (if applicable)
|
||||
# - Trigger fresh deploy of docs site, so changelog remains up-to-date
|
||||
# - Finally, shows summary of actions taken and new tag published
|
||||
name: 🔖 Auto Version & Tag
|
||||
#
|
||||
# Manual dispatch flow:
|
||||
# - If a version is provided, sets package.json to that version
|
||||
# - If no version is provided, increments patch version automatically
|
||||
# - Creates and pushes a git tag, then triggers docs site rebuild
|
||||
|
||||
name: 🔖 Tag
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to tag (e.g. 4.1.0). Leave blank to auto-increment patch.'
|
||||
required: false
|
||||
type: string
|
||||
# Btw, this is a safe and intentional trigger
|
||||
# It only runs once reviewed PR merged, and secrets are scoped
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [closed]
|
||||
branches: [master]
|
||||
|
||||
@@ -25,14 +39,37 @@ permissions:
|
||||
pull-requests: read
|
||||
issues: write
|
||||
|
||||
env:
|
||||
IS_MANUAL: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
jobs:
|
||||
version-and-tag:
|
||||
if: github.event.pull_request.merged == true
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|| github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Check PR for code changes and version bump 📂
|
||||
- name: 🔢 Validate manual dispatch
|
||||
if: env.IS_MANUAL == 'true'
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
DISPATCH_REF: ${{ github.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$DISPATCH_REF" != "refs/heads/master" ]; then
|
||||
echo "::error::Manual dispatch only allowed from master (got: $DISPATCH_REF)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$INPUT_VERSION" ] && ! printf '%s' "$INPUT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Invalid version '${INPUT_VERSION}'. Must be semver (e.g. 4.1.0)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 📂 Check PR for code changes and version bump
|
||||
id: check_pr
|
||||
if: env.IS_MANUAL != 'true'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
@@ -43,7 +80,7 @@ jobs:
|
||||
github.rest.pulls.listFiles, { owner, repo, pull_number }
|
||||
);
|
||||
const codePatterns = [
|
||||
/^src\//, /^services\//, /^public\//, /^Dockerfile$/, /^[^/]+\.js$/,
|
||||
/^src\//, /^services\//, /^public\//, /^Dockerfile$/, /^yarn.lock$/, /^[^/]+\.js$/,
|
||||
];
|
||||
const codeChanged = files.some(f =>
|
||||
codePatterns.some(p => p.test(f.filename))
|
||||
@@ -83,21 +120,21 @@ jobs:
|
||||
core.setOutput('needs_bump', needsBump.toString());
|
||||
core.setOutput('needs_tag', needsTag.toString());
|
||||
|
||||
- name: Checkout repository 🛎️
|
||||
if: steps.check_pr.outputs.needs_tag == 'true'
|
||||
- name: 🛎️ Checkout repository
|
||||
if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure git identity 👤
|
||||
if: steps.check_pr.outputs.needs_tag == 'true'
|
||||
- name: 👤 Configure git identity
|
||||
if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
|
||||
run: |
|
||||
git config user.name "Liss-Bot"
|
||||
git config user.email "liss-bot@d0h.co"
|
||||
|
||||
- name: Extract referenced issues 🔍
|
||||
- name: 🔍 Extract referenced issues
|
||||
id: issues
|
||||
if: steps.check_pr.outputs.needs_tag == 'true'
|
||||
if: env.IS_MANUAL != 'true' && steps.check_pr.outputs.needs_tag == 'true'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
@@ -119,47 +156,76 @@ jobs:
|
||||
core.info(`Found issue references: ${unique.join(', ')}`);
|
||||
core.setOutput('numbers', unique.join(','));
|
||||
|
||||
- name: Bump patch version ⬆️
|
||||
if: steps.check_pr.outputs.needs_bump == 'true'
|
||||
run: |
|
||||
npm version patch --no-git-tag-version
|
||||
git add package.json
|
||||
git commit -m "🔖 Bump version to $(node -p "require('./package.json').version")"
|
||||
git push
|
||||
|
||||
- name: Create and push tag 🏷️
|
||||
id: tag
|
||||
if: steps.check_pr.outputs.needs_tag == 'true'
|
||||
- name: ⬆️ Bump version
|
||||
id: bump
|
||||
if: >-
|
||||
env.IS_MANUAL == 'true'
|
||||
|| steps.check_pr.outputs.needs_bump == 'true'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$IS_MANUAL" = "true" ] && [ -n "$INPUT_VERSION" ]; then
|
||||
npm version "$INPUT_VERSION" --no-git-tag-version --allow-same-version
|
||||
else
|
||||
npm version patch --no-git-tag-version
|
||||
fi
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
git add package.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "package.json already at $NEW_VERSION, nothing to commit"
|
||||
echo "bumped=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git commit -m "🔖 Bump version to $NEW_VERSION"
|
||||
git push
|
||||
echo "bumped=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 🏷️ Create and push tag
|
||||
id: tag
|
||||
if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title || '' }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }}
|
||||
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
|
||||
ISSUES: ${{ steps.issues.outputs.numbers }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
git fetch --tags --force
|
||||
if git rev-parse "refs/tags/$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag $VERSION already exists, skipping"
|
||||
echo "result=existed" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'Dashy v%s 🚀\n\n' "$VERSION"
|
||||
printf 'PR: #%s - %s\n' "$PR_NUMBER" "$PR_TITLE"
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
printf 'PR: #%s - %s\n' "$PR_NUMBER" "$PR_TITLE"
|
||||
else
|
||||
printf 'Manual release by @%s\n' "$PR_AUTHOR"
|
||||
fi
|
||||
if [ -n "$ISSUES" ]; then
|
||||
printf 'Resolves: %s\n' "$(echo "$ISSUES" | sed 's/,/, #/g; s/^/#/')"
|
||||
fi
|
||||
printf 'Author: @%s\n' "$PR_AUTHOR"
|
||||
printf 'Merge commit: %s\n' "$MERGE_SHA"
|
||||
printf 'Commit: %s\n' "$MERGE_SHA"
|
||||
} > tag-message.txt
|
||||
|
||||
git tag -a "$VERSION" -F tag-message.txt
|
||||
git push origin "$VERSION"
|
||||
echo "result=created" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Label referenced issues 🛩️
|
||||
- name: 🛩️ Label referenced issues
|
||||
id: label
|
||||
if: steps.check_pr.outputs.needs_tag == 'true' && steps.issues.outputs.numbers != ''
|
||||
if: >-
|
||||
env.IS_MANUAL != 'true'
|
||||
&& steps.check_pr.outputs.needs_tag == 'true'
|
||||
&& steps.issues.outputs.numbers != ''
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
@@ -234,53 +300,65 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
- name: Trigger docs site rebuild 📝
|
||||
- name: 📝 Trigger docs site rebuild
|
||||
id: docs
|
||||
if: steps.tag.outcome == 'success'
|
||||
if: steps.tag.outputs.result == 'created'
|
||||
continue-on-error: true
|
||||
env:
|
||||
HOOK_URL: ${{ secrets.DOCS_SITE_REBUILD_HOOK }}
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$HOOK_URL" ]; then
|
||||
echo "::warning::DOCS_SITE_REBUILD_HOOK secret is not set, skipping"
|
||||
exit 1
|
||||
fi
|
||||
VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")
|
||||
curl -sf -X POST -d '{}' "${HOOK_URL}?trigger_title=v${VERSION}+released" \
|
||||
--max-time 15 --retry 2 --retry-max-time 30
|
||||
echo "Triggered docs rebuild for v${VERSION}"
|
||||
|
||||
- name: Job summary 📋
|
||||
- name: 📋 Job summary
|
||||
if: always()
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title || '' }}
|
||||
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||
NEEDS_BUMP: ${{ steps.check_pr.outputs.needs_bump }}
|
||||
NEEDS_TAG: ${{ steps.check_pr.outputs.needs_tag }}
|
||||
ISSUES: ${{ steps.issues.outputs.numbers }}
|
||||
BUMPED: ${{ steps.bump.outputs.bumped }}
|
||||
TAG_OUTCOME: ${{ steps.tag.outcome }}
|
||||
TAG_RESULT: ${{ steps.tag.outputs.result }}
|
||||
TAG_VERSION: ${{ steps.tag.outputs.version }}
|
||||
LABEL_OUTCOME: ${{ steps.label.outcome }}
|
||||
DOCS_OUTCOME: ${{ steps.docs.outcome }}
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")
|
||||
set -euo pipefail
|
||||
VERSION="${TAG_VERSION:-$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")}"
|
||||
|
||||
{
|
||||
echo "## 🔖 Auto Version & Tag"
|
||||
echo ""
|
||||
echo "| Step | Result |"
|
||||
echo "|------|--------|"
|
||||
echo "| PR | [#${PR_NUMBER}](${REPO_URL}/pull/${PR_NUMBER}) — ${PR_TITLE} |"
|
||||
|
||||
if [ "$NEEDS_BUMP" = "true" ]; then
|
||||
if [ "$IS_MANUAL" = "true" ]; then
|
||||
echo "| Trigger | Manual dispatch |"
|
||||
elif [ -n "$PR_NUMBER" ]; then
|
||||
echo "| PR | [#${PR_NUMBER}](${REPO_URL}/pull/${PR_NUMBER}) — ${PR_TITLE} |"
|
||||
fi
|
||||
|
||||
if [ "$BUMPED" = "true" ]; then
|
||||
echo "| Version bump | ✅ \`${VERSION}\` |"
|
||||
else
|
||||
echo "| Version bump | ⏭️ Skipped |"
|
||||
fi
|
||||
|
||||
if [ "$NEEDS_TAG" = "true" ] && [ "$TAG_OUTCOME" = "success" ]; then
|
||||
if [ "$TAG_RESULT" = "created" ]; then
|
||||
echo "| Tag | ✅ [\`${VERSION}\`](${REPO_URL}/releases/tag/${VERSION}) |"
|
||||
elif [ "$NEEDS_TAG" = "true" ]; then
|
||||
elif [ "$TAG_RESULT" = "existed" ]; then
|
||||
echo "| Tag | ⏭️ Already exists: \`${VERSION}\` |"
|
||||
elif [ "$TAG_OUTCOME" = "failure" ]; then
|
||||
echo "| Tag | ❌ Failed |"
|
||||
else
|
||||
echo "| Tag | ⏭️ Skipped |"
|
||||
4
.github/workflows/update-docs-site.yml
vendored
4
.github/workflows/update-docs-site.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 📝 Update Documentation
|
||||
name: 📝 Sync Docs
|
||||
|
||||
# This will run whenever the /docs directory in master branch is updated,
|
||||
# or if the workflow is manually dispatched, plus a sync check on Sun at 03:30 UTC
|
||||
@@ -12,6 +12,8 @@ on:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
# Jobs to be run:
|
||||
# 1. Checkout master branch
|
||||
|
||||
4
.github/workflows/wiki-sync.yml
vendored
4
.github/workflows/wiki-sync.yml
vendored
@@ -4,6 +4,10 @@ on:
|
||||
workflow_dispatch: # Manual dispatch
|
||||
schedule:
|
||||
- cron: '0 1 * * 0' # At 01:00 on Sunday.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
17
.github/zizmor.yml
vendored
Normal file
17
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# zizmor config, for the static analysis of our GH Actions workflows
|
||||
# Run with: `zizmor .github/workflows/`. Docs: https://docs.zizmor.sh
|
||||
# Scan is also runs in our ci.yml, whenever workflow changes are made
|
||||
|
||||
# 2 rules are disabled right now:
|
||||
# - unpinned-uses: The only unpinned versions are official ones.
|
||||
# And in the context of each, there shouldn't be any risk.
|
||||
# Pinning causes the need to update each whenever a new version or patch is released
|
||||
# - artipacked: Too many false positives.
|
||||
# Was flagging every `actions/checkout` for not having `persist-credentials: false`
|
||||
# Even tho we never upload or do anything public with the checkout artifacts
|
||||
|
||||
rules:
|
||||
unpinned-uses:
|
||||
disable: true
|
||||
artipacked:
|
||||
disable: true
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,9 @@
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# Dashy config backups
|
||||
user-data/config-backups/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
42
LICENSE
42
LICENSE
@@ -1,21 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2024 Alicia Sykes <https://aliciasykes.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2026 Alicia Sykes <https://aliciasykes.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
> [!IMPORTANT]
|
||||
> Dashy's built-in auth is not intended to protect a publicly hosted instance against unauthorized access. Instead you should use an auth provider compatible with your reverse proxy, or access Dashy via your VPN, or implement your own SSO logic.
|
||||
>
|
||||
> If Dashy is only accessible within your home network and you just want a login page, then the built-in auth may be sufficient. To also protect server-side endpoints and config files, set `ENABLE_HTTP_AUTH=true` (see [Adding HTTP Auth to Configuration](#adding-http-auth-to-configuration)).
|
||||
> If Dashy is only accessible within your home network and you just want a login page, then the built-in auth may be sufficient. To also protect server-side endpoints and config files: with built-in auth set `ENABLE_HTTP_AUTH=true` ([details](#adding-http-auth-to-configuration)). (Or, consider setting up[OIDC](#oidc), [Keycloak](#keycloak), or [Header Auth](#header-authentication), where the server-side enforcement is on automatically).
|
||||
|
||||
## Built-In Auth
|
||||
|
||||
@@ -210,11 +210,13 @@ Use the following run command, replacing the attributes (default credentials, po
|
||||
docker run -d \
|
||||
-p 8081:8080 \
|
||||
--name auth-server \
|
||||
-e KEYCLOAK_USER=admin \
|
||||
-e KEYCLOAK_PASSWORD=admin \
|
||||
quay.io/keycloak/keycloak:15.0.2
|
||||
-e KEYCLOAK_ADMIN=admin \
|
||||
-e KEYCLOAK_ADMIN_PASSWORD=admin \
|
||||
quay.io/keycloak/keycloak:25.0 start-dev
|
||||
```
|
||||
|
||||
(The `KEYCLOAK_USER` / `KEYCLOAK_PASSWORD` env vars and the `/auth` URL prefix from Keycloak 16 and older have been replaced. If you are still on 17 or older, set `legacySupport: true` in your Dashy config later on.)
|
||||
|
||||
If you need to pull from DockerHub, a non-official image is available [here](https://registry.hub.docker.com/r/jboss/keycloak). Or if you would prefer not to use Docker, you can also directly install Keycloak from source, following [this guide](https://www.keycloak.org/docs/latest/getting_started/index.html).
|
||||
|
||||
You should now be able to access the Keycloak web interface, using the port specified above (e.g. `http://127.0.0.1:8081`), login with the default credentials, and when prompted create a new password.
|
||||
@@ -230,14 +232,27 @@ Before we can use Keycloak, we must first set it up with some users. Keycloak us
|
||||
You can now create your first user.
|
||||
|
||||
1. In the left-hand menu, click 'Users', then 'Add User'
|
||||
2. Fill in the form, including username and hit 'Save'
|
||||
2. Fill in the form. On Keycloak 25 and newer, *First name* and *Last name* are required by the default user-profile schema. If you skip them the user can sign in but login will then fail with "Account is not fully set up"
|
||||
3. Under the 'Credentials' tab, give the new user an initial password. They will be prompted to change this after first login
|
||||
|
||||
The last thing we need to do in the Keycloak admin console is to create a new client
|
||||
Next, create a new client for Dashy.
|
||||
|
||||
1. Within your new realm, navigate to 'Clients' on the left-hand side, then click 'Create' in the top-right
|
||||
2. Choose a 'Client ID', set 'Client Protocol' to 'openid-connect', and for 'Valid Redirect URIs' put a URL pattern to where you're hosting Dashy (if you're just testing locally, then * is fine), and do the same for the 'Web Origins' field
|
||||
3. Make note of your client-id, and click 'Save'
|
||||
2. Choose a 'Client ID' (e.g. `dashy`), set 'Client Protocol' to 'openid-connect'
|
||||
3. Turn *Client authentication* OFF and leave *Standard flow* enabled. Dashy is a SPA, so it acts as an OAuth public client with PKCE. A confidential client requires a client_secret that a browser app can't safely hold
|
||||
4. For 'Valid Redirect URIs' put the URL where you host Dashy (e.g. `https://dashy.example.com/*`, or just `*` while testing locally). Do the same for the 'Web Origins' field
|
||||
5. Make note of your client-id, and click 'Save'
|
||||
|
||||
For the `adminRole` check to work, the role must appear in the id_token (Keycloak's default mapper only adds it to the access token):
|
||||
|
||||
1. Open your `dashy` client, go to the *Client scopes* tab, click the dedicated scope row (`dashy-dedicated`)
|
||||
2. Add a new mapper of type *User Realm Role*, name it (e.g. `realm_roles`), claim name `realm_access.roles`, multivalued ON, *Add to ID token* ON, *Add to access token* ON
|
||||
3. (Optional, for `adminGroup` instead of `adminRole`) Add a second mapper of type *Group Membership*, claim name `groups`
|
||||
|
||||
To create the admin role itself and grant it to a user:
|
||||
|
||||
1. *Realm roles* in the left-hand menu, *Create role*, name it (e.g. `dashy-admin`)
|
||||
2. *Users* → pick your admin user → *Role mapping* → *Assign role* → select `dashy-admin`
|
||||
|
||||
### 3. Enable Keycloak in Dashy Config File
|
||||
|
||||
@@ -246,13 +261,15 @@ For example:
|
||||
|
||||
```yaml
|
||||
appConfig:
|
||||
...
|
||||
# ...
|
||||
disableConfigurationForNonAdmin: true
|
||||
auth:
|
||||
enableKeycloak: true
|
||||
keycloak:
|
||||
serverUrl: 'http://localhost:8081'
|
||||
realm: 'alicia-homelab'
|
||||
clientId: 'dashy'
|
||||
adminRole: 'dashy-admin' # role name that grants admin privileges
|
||||
```
|
||||
|
||||
Note that if you are using Keycloak V 17 or older, you will also need to set `legacySupport: true` (also under `appConfig.auth.keycloak`). This is because the API endpoint was updated in later versions.
|
||||
@@ -281,12 +298,19 @@ sections:
|
||||
groups: ['DevelopmentTeam']
|
||||
```
|
||||
|
||||
Depending on how you're hosting Dashy and Keycloak, you may also need to set some HTTP headers, to prevent a CORS error. This would typically be the `Access-Control-Allow-Origin [URL-of Dashy]` on your Keycloak instance. See the [Setting Headers](https://github.com/Lissy93/dashy/blob/master/docs/management.md#setting-headers) guide in the management docs for more info.
|
||||
|
||||
Your app is now secured :) When you load Dashy, it will redirect to your Keycloak login page, and any user without valid credentials will be prevented from accessing your dashboard.
|
||||
|
||||
From within the Keycloak console, you can then configure things like time-outs, password policies, etc. You can also backup your full Keycloak config, and it is recommended to do this, along with your Dashy config. You can spin up both Dashy and Keycloak simultaneously and restore both applications configs using a `docker-compose.yml` file, and this is recommended.
|
||||
|
||||
### CORS Headers
|
||||
If Dashy and Keycloak run on different origins (typical when testing locally on different `localhost:` ports), Keycloak's default `Content-Security-Policy: frame-ancestors 'self'` and `X-Frame-Options: SAMEORIGIN` block the hidden iframe `keycloak-js` uses to check your session. Symptom: a generic "Authentication failed (Keycloak)" toast on first load. To allow the iframe, open *Realm settings → Security defenses → Browser headers*, clear `X-Frame-Options`, and change `Content-Security-Policy` to `frame-src 'self' <your-dashy-origin>; frame-ancestors 'self' <your-dashy-origin>; object-src 'none';`. Same-origin production deployments don't hit this.
|
||||
|
||||
### How server-side enforcement works
|
||||
|
||||
Dashy's server reads `auth.keycloak` from `conf.yml` at boot, lazily fetches your Keycloak realm's OIDC discovery doc + JWKS, then verifies the `id_token` the SPA attaches to every API call as `Authorization: Bearer <id_token>`. Tokens that fail signature / issuer / audience / expiry verification are rejected with `401`. Write endpoints (`POST /config-manager/save`) additionally require the `adminRole` (or `adminGroup`) to be present in the token claims, and non-admins receive `403`. Note that the `/conf.yml` remains anonymously readable still (so the SPA can bootstrap before login).
|
||||
|
||||
The admin check reads the role / group claim from the id_token, so the client mapper from Step 2 above (the one with *Add to ID token* on) is what makes `adminRole` / `adminGroup` work. Without it the server gets a token with no roles claim and treats everyone as non-admin.
|
||||
|
||||
### Troubleshooting Keycloak
|
||||
|
||||
If you encounter issues with your Keycloak setup, follow these steps to troubleshoot and resolve common problems.
|
||||
@@ -357,6 +381,7 @@ Dashy also supports using a general [OIDC compatible](https://openid.net/connect
|
||||
|
||||
```yaml
|
||||
appConfig:
|
||||
disableConfigurationForNonAdmin: true # Prevent authenticated non-admins using editor
|
||||
auth:
|
||||
enableOidc: true
|
||||
oidc:
|
||||
@@ -368,6 +393,8 @@ appConfig:
|
||||
|
||||
Because Dashy is a SPA, a [public client](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1) registration with PKCE is needed.
|
||||
|
||||
If you set `adminGroup`, include `groups` in `scope` (e.g. `scope: 'openid profile email groups'`) so your IdP actually returns the claim in the id_token. Same goes for `adminRole` and a `roles` scope if your IdP needs one.
|
||||
|
||||
Note, that if your `clientId` is numeric, you must place it in quotes. Otherwise it will be interpreted as a number and truncated to 64 chars!
|
||||
|
||||
An example for Authelia is shared below, but other OIDC systems can be used:
|
||||
@@ -396,6 +423,12 @@ identity_providers:
|
||||
|
||||
Groups and roles will be populated and available for controlling display similar to [Keycloak](#Keycloak) above.
|
||||
|
||||
### How server-side enforcement works
|
||||
|
||||
Dashy's server reads `auth.oidc` from `conf.yml` at boot, lazily fetches the OIDC discovery doc + JWKS from your `endpoint`, then verifies the `id_token` the SPA attaches to every API call as `Authorization: Bearer <id_token>`. Tokens that fail signature / issuer / audience / expiry verification are rejected with `401`. Write endpoints (`POST /config-manager/save`) additionally require the `adminGroup` (or `adminRole`) to be present in the token's `groups` / `roles` claims, and non-admins receive `403`. Note that the `/conf.yml` remains anonymously readable still
|
||||
|
||||
Your IdP must include `groups` / `roles` in the id_token, not only the access token, for the admin check to work (most IdPs do this when the `groups` scope is requested).
|
||||
|
||||
---
|
||||
|
||||
## authentik
|
||||
@@ -433,7 +466,7 @@ A dialog box will pop-up, select the `OAuth2/OpenID Provider`. Click `Next`.
|
||||
|
||||

|
||||
|
||||
On the next page of the wizard, set the `Name`, `Authentication flow`, and `Authorization flow`. See example below. Using the `default-provider-authorization-implicit-consent` authorization flow on internal services and `default-provider-authorization-explicit-consent` on external services is a common practice. However, it is fully up to you on how you would like to configure this option. `Implicit` will login directly without user consent, `explicit` will ask if the user approves the service being logged into with their user credentials.
|
||||
On the next page of the wizard, set the `Name`, `Authentication flow`, `Authorization flow`, and `Invalidation flow`. See example below. Using the `default-provider-authorization-implicit-consent` authorization flow on internal services and `default-provider-authorization-explicit-consent` on external services is a common practice. However, it is fully up to you on how you would like to configure this option. `Implicit` will login directly without user consent, `explicit` will ask if the user approves the service being logged into with their user credentials. For the invalidation flow (required on Authentik 2023.10 and later) the built-in `default-provider-invalidation-flow` is fine.
|
||||
|
||||

|
||||
|
||||
@@ -448,7 +481,15 @@ Scroll down to set the `Signing Key`. It is recommended to use the built in `aut
|
||||
|
||||

|
||||
|
||||
Expand `Advanced protocol settings` then verify the `Scopes` are set to what is highlighted in `white` below. Set the `Subject mode` to `Based on the Users's Email`.
|
||||
If you plan to use `adminGroup` in your Dashy config, you need a `groups` scope mapping first. Authentik does not ship one by default. Open *Customisation > Property Mappings* in a new tab, click *Create > Scope Mapping*, set *Name* to `groups`, *Scope name* to `groups`, and *Expression* to:
|
||||
|
||||
```python
|
||||
return {"groups": [g.name for g in request.user.ak_groups.all()]}
|
||||
```
|
||||
|
||||
Save it, then come back to the provider wizard.
|
||||
|
||||
Expand `Advanced protocol settings` then verify the `Scopes` are set to what is highlighted in `white` below (including the `groups` mapping you just created, if you want `adminGroup` to work). Set the `Subject mode` to `Based on the Users's Email`.
|
||||
|
||||

|
||||
|
||||
@@ -507,11 +548,12 @@ Enter the `Client ID` in the `clientId` field and `OpenID Configuration Issuer`
|
||||
|
||||
Below is how to configure the `auth` section in the yaml syntax. Once this is enabled, when an attempt to access `Dashy` is made it will now redirect you to the `authentik` login page moving forward.
|
||||
|
||||
```
|
||||
```yaml
|
||||
appConfig:
|
||||
theme: glass
|
||||
layout: auto
|
||||
iconSize: medium
|
||||
disableConfigurationForNonAdmin: true # Prevent logged-in, non-admins using the view/edit config features
|
||||
auth:
|
||||
enableOidc: true
|
||||
oidc:
|
||||
|
||||
@@ -79,6 +79,25 @@ If you are not comfortable with making pull requests, or do not want to modify t
|
||||
|
||||
---
|
||||
|
||||
## Checking Translation Coverage
|
||||
|
||||
We've got a short test/lint script for verifying translation files are complete and valid, and to show per-language coverage report.
|
||||
|
||||
```bash
|
||||
yarn validate-locales
|
||||
```
|
||||
|
||||
This will run the following checks:
|
||||
- Ensure all locale files are registered, present and parsable (failure)
|
||||
- Non-existing translations used in the code, not present in en.json (failure)
|
||||
- Translations in en.json never used anywhere in the code (warn)
|
||||
- Translations in other locales not used in en.json or code (warn)
|
||||
- Missing translations in other locales compared to en.json (coverage report)
|
||||
|
||||
This script is located in [`tests/locales/check-locales.js`](https://github.com/lissy93/dashy/blob/master/tests/locales/check-locales.js), and can be executed directly, or with `yarn validate-locales` and is also run as part of the CI workflow on PRs.
|
||||
|
||||
---
|
||||
|
||||
## Adding New Text to a Component
|
||||
|
||||
If you're working on a new component, then any text that is displayed to the user should be extracted out of the component, and stored in the file. This also applies to any existing components, that might have been forgotten to be translated. Thankfully, everything is already setup, so this is a pretty easy job.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -160,10 +160,11 @@ A simple, live-updating local weather component, showing temperature, conditions
|
||||
--- | --- | --- | ---
|
||||
**`apiKey`** | `string` | Required | Your OpenWeatherMap API key. You can get one for free at [openweathermap.org](https://openweathermap.org/)
|
||||
**`city`** | `string` | Required | A city name to use for fetching weather. This can also be a state code or country code, following the ISO-3166 format
|
||||
**`cityId`** | `number` | _Optional_ | An OpenWeatherMap numeric city ID, used to disambiguate cities that share a name. You can find the ID in the URL of the city's page on [openweathermap.org](https://openweathermap.org/) (e.g. `2643743` for London, GB). If provided, this will override the `city` option
|
||||
**`units`** | `string` | _Optional_ | The units to use for displaying data, can be either `metric` or `imperial`. Defaults to `metric`
|
||||
**`hideDetails`** | `boolean` | _Optional_ | If set to `true`, the additional details (wind, humidity, pressure, etc) will not be shown. Defaults to `false`
|
||||
**`lat`** | `number` | _Optional_ | To show weather for a specific location, you can provide the latitude and longitude coordinates. If provided, this will override the `city` option
|
||||
**`lon`** | `number` | _Optional_ | To show weather for a specific location, you can provide the latitude and longitude coordinates. If provided, this will override the `city` option
|
||||
**`lat`** | `number` | _Optional_ | To show weather for a specific location, you can provide the latitude and longitude coordinates. If provided, this will override the `city` and `cityId` options
|
||||
**`lon`** | `number` | _Optional_ | To show weather for a specific location, you can provide the latitude and longitude coordinates. If provided, this will override the `city` and `cityId` options
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -197,6 +198,9 @@ Displays the weather (temperature and conditions) for the next few days for a gi
|
||||
--- | --- | --- | ---
|
||||
**`apiKey`** | `string` | Required | Your OpenWeatherMap API key. You can get one at [openweathermap.org](https://openweathermap.org/) or for free via the [OWM Student Plan](https://home.openweathermap.org/students)
|
||||
**`city`** | `string` | Required | A city name to use for fetching weather. This can also be a state code or country code, following the ISO-3166 format
|
||||
**`cityId`** | `number` | _Optional_ | An OpenWeatherMap numeric city ID, used to disambiguate cities that share a name. You can find the ID in the URL of the city's page on [openweathermap.org](https://openweathermap.org/) (e.g. `2643743` for London, GB). If provided, this will override the `city` option
|
||||
**`lat`** | `number` | _Optional_ | Latitude for a specific location. If provided alongside `lon`, this will override the `city` and `cityId` options
|
||||
**`lon`** | `number` | _Optional_ | Longitude for a specific location. If provided alongside `lat`, this will override the `city` and `cityId` options
|
||||
**`numDays`** | `number` | _Optional_ | The number of days to display of forecast info to display. Defaults to `4`, max `16` days
|
||||
**`units`** | `string` | _Optional_ | The units to use for displaying data, can be either `metric` or `imperial`. Defaults to `metric`
|
||||
**`hideDetails`** | `boolean` | _Optional_ | If set to `true`, the additional details (wind, humidity, pressure, etc) will not be shown. Defaults to `false`
|
||||
|
||||
25
package.json
25
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dashy",
|
||||
"version": "4.0.8",
|
||||
"version": "4.0.10",
|
||||
"license": "MIT",
|
||||
"main": "server",
|
||||
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
||||
@@ -10,6 +10,7 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint \"src/**/*.{js,vue}\"",
|
||||
"validate-locales": "node tests/locales/check-locales.js",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
@@ -28,7 +29,7 @@
|
||||
"@codemirror/lint": "^6.9.6",
|
||||
"@codemirror/search": "^6.7.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.41.1",
|
||||
"@codemirror/view": "^6.43.0",
|
||||
"@jsonforms/core": "^3.7.0",
|
||||
"@jsonforms/vue": "^3.7.0",
|
||||
"@jsonforms/vue-vanilla": "^3.7.0",
|
||||
@@ -37,10 +38,11 @@
|
||||
"ajv": "^8.20.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dompurify": "^3.4.1",
|
||||
"express": "^4.21.0",
|
||||
"dompurify": "^3.4.3",
|
||||
"express": "^4.22.2",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"frappe-charts": "^1.6.2",
|
||||
"jose": "^5.10.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"keycloak-js": "^26.0.0",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
@@ -51,16 +53,16 @@
|
||||
"vue-router": "^4.4.0",
|
||||
"vue-select": "4.0.0-beta.6",
|
||||
"vuex": "^4.1.0",
|
||||
"yaml": "^2.8.3"
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"@vitest/ui": "^4.1.5",
|
||||
"@vitest/ui": "^4.1.6",
|
||||
"@vue/compiler-sfc": "^3.5.0",
|
||||
"@vue/test-utils": "^2.4.8",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint": "^10.4.0",
|
||||
"eslint-plugin-import-x": "^4.16.2",
|
||||
"eslint-plugin-vue": "^10.9.1",
|
||||
"globals": "^17.5.0",
|
||||
@@ -71,9 +73,9 @@
|
||||
"vite": "^6.2.0",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitest": "^4.1.5",
|
||||
"vitest": "^4.1.6",
|
||||
"vue-eslint-parser": "^10.0.0",
|
||||
"vue-tsc": "^3.2.8"
|
||||
"vue-tsc": "^3.2.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -90,9 +92,10 @@
|
||||
"resolutions": {
|
||||
"braces": "^3.0.3",
|
||||
"micromatch": "^4.0.8",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.5.14",
|
||||
"serialize-javascript": "^7.0.3",
|
||||
"vite": "^6.2.0"
|
||||
"vite": "^6.2.0",
|
||||
"@babel/plugin-transform-modules-systemjs": "^7.29.4"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const systemInfo = require('./system-info'); // Basic system info, for resource
|
||||
const sslServer = require('./ssl-server'); // TLS-enabled web server
|
||||
const corsProxy = require('./cors-proxy'); // Enables API requests to CORS-blocked services
|
||||
const getUser = require('./get-user'); // Enables server side user lookup
|
||||
const { loadOidcSettings, createOidcMiddleware } = require('./auth-oidc'); // Server-side OIDC/Keycloak token verification
|
||||
|
||||
/* Service endpoint URL paths (see also serviceEndpoints in src/utils/config/defaults.js) */
|
||||
const ENDPOINTS = {
|
||||
@@ -78,7 +79,7 @@ process.on('unhandledRejection', (reason) => {
|
||||
/* Load appConfig.auth from config (if present) for authorization purposes */
|
||||
function loadAuthConfig() {
|
||||
try {
|
||||
const filePath = path.join(rootDir, process.env.USER_DATA_DIR || 'user-data', 'conf.yml');
|
||||
const filePath = path.resolve(rootDir, process.env.USER_DATA_DIR || 'user-data', 'conf.yml');
|
||||
const fileContents = fs.readFileSync(filePath, 'utf8');
|
||||
const data = yaml.load(fileContents);
|
||||
return data?.appConfig?.auth || {};
|
||||
@@ -116,9 +117,9 @@ function customAuthorizer(username, password) {
|
||||
}
|
||||
}
|
||||
|
||||
/* If auth is enabled, setup auth for config access, otherwise skip */
|
||||
function getBasicAuthMiddleware() {
|
||||
const authConfig = loadAuthConfig();
|
||||
/* Pick an auth strategy based on what's configured in conf.yml + env.
|
||||
OIDC / Keycloak takes precedence — it's the strongest enforcement we offer. */
|
||||
function getAuthMiddleware(authConfig, oidcSettings) {
|
||||
const confUsers = authConfig.users || null;
|
||||
const hasConfUsers = confUsers && confUsers.length > 0;
|
||||
const useConfAuth = process.env.ENABLE_HTTP_AUTH && hasConfUsers;
|
||||
@@ -133,7 +134,9 @@ function getBasicAuthMiddleware() {
|
||||
+ ' This will cause auth failures. Set ENABLE_HTTP_AUTH=true, or remove users from conf.yml.');
|
||||
}
|
||||
|
||||
if (useConfAuth) {
|
||||
if (oidcSettings) {
|
||||
return createOidcMiddleware(oidcSettings);
|
||||
} else if (useConfAuth) {
|
||||
return basicAuth({
|
||||
authorizer: customAuthorizer,
|
||||
challenge: true,
|
||||
@@ -163,11 +166,37 @@ function getBasicAuthMiddleware() {
|
||||
return (req, res, next) => next();
|
||||
}
|
||||
|
||||
const protectConfig = getBasicAuthMiddleware();
|
||||
const initialAuthConfig = loadAuthConfig();
|
||||
const oidcSettings = loadOidcSettings(initialAuthConfig);
|
||||
const protectConfig = getAuthMiddleware(initialAuthConfig, oidcSettings);
|
||||
|
||||
/* Middleware to restrict write endpoints to admin users only */
|
||||
/* True when any auth method is configured. Used to keep zero-auth deployments
|
||||
open (their original behaviour) while closing the gate for everyone else. */
|
||||
const authIsConfigured = Boolean(
|
||||
oidcSettings
|
||||
|| (process.env.ENABLE_HTTP_AUTH && initialAuthConfig.users?.length)
|
||||
|| (process.env.BASIC_AUTH_USERNAME && process.env.BASIC_AUTH_PASSWORD)
|
||||
|| (initialAuthConfig.enableHeaderAuth && initialAuthConfig.headerAuth),
|
||||
);
|
||||
|
||||
/* Require an authenticated identity on this request. No-op for zero-auth deploys. */
|
||||
function requireAuth(req, res, next) {
|
||||
if (!authIsConfigured) return next();
|
||||
if (req.auth) return next();
|
||||
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
/* Restrict to admin users. OIDC/Keycloak get isAdmin from token claims; the
|
||||
conf.yml `users[]` path falls back to looking up user.type === 'admin'. */
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.auth) return next();
|
||||
if (!authIsConfigured) return next();
|
||||
if (!req.auth) {
|
||||
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
}
|
||||
if (typeof req.auth.isAdmin === 'boolean') {
|
||||
if (req.auth.isAdmin) return next();
|
||||
return res.status(403).json({ success: false, message: 'Forbidden - Admin access required' });
|
||||
}
|
||||
const users = loadUserConfig();
|
||||
if (!users || users.length === 0) return next();
|
||||
const user = users.find(u => u.user.toLowerCase() === req.auth.user.toLowerCase());
|
||||
@@ -191,7 +220,7 @@ const app = express()
|
||||
// Load middlewares for parsing JSON, and supporting HTML5 history routing
|
||||
.use(express.json({ limit: '1mb' }))
|
||||
// GET endpoint to run status of a given URL with GET request
|
||||
.use(ENDPOINTS.statusCheck, protectConfig, method('GET', (req, res) => {
|
||||
.use(ENDPOINTS.statusCheck, protectConfig, requireAuth, method('GET', (req, res) => {
|
||||
try {
|
||||
statusCheck(req.url, (results) => {
|
||||
if (!res.headersSent) {
|
||||
@@ -223,7 +252,7 @@ const app = express()
|
||||
});
|
||||
}))
|
||||
// GET endpoint to return system info, for widget
|
||||
.use(ENDPOINTS.systemInfo, protectConfig, method('GET', (req, res) => {
|
||||
.use(ENDPOINTS.systemInfo, protectConfig, requireAuth, method('GET', (req, res) => {
|
||||
try {
|
||||
safeEnd(res, JSON.stringify(systemInfo()));
|
||||
} catch (e) {
|
||||
@@ -231,7 +260,7 @@ const app = express()
|
||||
}
|
||||
}))
|
||||
// GET for accessing non-CORS API services
|
||||
.use(ENDPOINTS.corsProxy, protectConfig, (req, res) => {
|
||||
.use(ENDPOINTS.corsProxy, protectConfig, requireAuth, (req, res) => {
|
||||
try {
|
||||
corsProxy(req, res);
|
||||
} catch (e) {
|
||||
@@ -239,7 +268,7 @@ const app = express()
|
||||
}
|
||||
})
|
||||
// GET endpoint to return user info
|
||||
.use(ENDPOINTS.getUser, protectConfig, method('GET', (req, res) => {
|
||||
.use(ENDPOINTS.getUser, protectConfig, requireAuth, method('GET', (req, res) => {
|
||||
try {
|
||||
safeEnd(res, JSON.stringify(getUser(config, req)));
|
||||
} catch (e) {
|
||||
@@ -249,13 +278,13 @@ const app = express()
|
||||
// Middleware to serve any .yml files in USER_DATA_DIR with optional protection
|
||||
.get('/*.yml', protectConfig, (req, res) => {
|
||||
const ymlFile = req.path.split('/').pop();
|
||||
const filePath = path.join(rootDir, process.env.USER_DATA_DIR || 'user-data', ymlFile);
|
||||
const filePath = path.resolve(rootDir, process.env.USER_DATA_DIR || 'user-data', ymlFile);
|
||||
res.sendFile(filePath, (err) => {
|
||||
if (err) safeEnd(res, errBody(`Could not read ${ymlFile}`), 404);
|
||||
});
|
||||
})
|
||||
// Serves up static files
|
||||
.use(express.static(path.join(rootDir, process.env.USER_DATA_DIR || 'user-data')))
|
||||
.use(express.static(path.resolve(rootDir, process.env.USER_DATA_DIR || 'user-data')))
|
||||
.use(express.static(path.join(rootDir, 'dist')))
|
||||
.use(express.static(path.join(rootDir, 'public'), { index: 'initialization.html' }))
|
||||
// If no other route is matched, serve up the index.html with a 404 status
|
||||
|
||||
140
services/auth-oidc.js
Normal file
140
services/auth-oidc.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Server-side OIDC / Keycloak token verification.
|
||||
*
|
||||
* Why this exists: Dashy's OIDC + Keycloak flows are otherwise entirely
|
||||
* client-side, so the server never sees an identity. This module verifies the
|
||||
* id_token a logged-in browser attaches to API requests, so admin-gated
|
||||
* endpoints (and authenticated endpoints generally) can enforce auth server-side.
|
||||
*
|
||||
* The middleware is permissive on a missing Authorization header — bootstrap
|
||||
* fetches (e.g. /conf.yml) must succeed before the user is logged in. Endpoints
|
||||
* that require auth should be paired with `requireAuth` (in app.js).
|
||||
* A present-but-invalid token is always rejected.
|
||||
*/
|
||||
|
||||
const { createRemoteJWKSet, jwtVerify } = require('jose');
|
||||
|
||||
/* Normalise the OIDC / Keycloak block from conf.yml into a unified shape.
|
||||
Returns null when neither provider is enabled or required fields are missing. */
|
||||
function loadOidcSettings(authConfig) {
|
||||
if (!authConfig || typeof authConfig !== 'object') return null;
|
||||
|
||||
if (authConfig.enableOidc && authConfig.oidc) {
|
||||
const { endpoint, clientId, adminGroup, adminRole } = authConfig.oidc;
|
||||
if (!endpoint || !clientId) return null;
|
||||
return {
|
||||
kind: 'oidc',
|
||||
issuer: String(endpoint),
|
||||
clientId: String(clientId),
|
||||
adminGroup: adminGroup || null,
|
||||
adminRole: adminRole || null,
|
||||
};
|
||||
}
|
||||
|
||||
if (authConfig.enableKeycloak && authConfig.keycloak) {
|
||||
const {
|
||||
serverUrl, realm, clientId, adminGroup, adminRole, legacySupport,
|
||||
} = authConfig.keycloak;
|
||||
if (!serverUrl || !realm || !clientId) return null;
|
||||
// Mirror the URL keycloak-js builds in the browser.
|
||||
const base = (legacySupport ? `${serverUrl}/auth` : serverUrl).replace(/\/$/, '');
|
||||
return {
|
||||
kind: 'keycloak',
|
||||
issuer: `${base}/realms/${realm}`,
|
||||
clientId: String(clientId),
|
||||
adminGroup: adminGroup || null,
|
||||
adminRole: adminRole || null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Per-issuer cache: discovery metadata + JWKS resolver. createRemoteJWKSet
|
||||
handles key caching + rotation internally. */
|
||||
const issuerCache = new Map();
|
||||
|
||||
async function fetchDiscovery(issuer) {
|
||||
const base = issuer.endsWith('/') ? issuer : `${issuer}/`;
|
||||
const url = new URL('.well-known/openid-configuration', base);
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`OIDC discovery returned ${res.status} for ${url}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function getIssuerContext(issuer) {
|
||||
const cached = issuerCache.get(issuer);
|
||||
if (cached) return cached;
|
||||
const config = await fetchDiscovery(issuer);
|
||||
if (!config.issuer || !config.jwks_uri) {
|
||||
throw new Error('Discovery document is missing `issuer` or `jwks_uri`');
|
||||
}
|
||||
const ctx = {
|
||||
canonicalIssuer: config.issuer,
|
||||
jwks: createRemoteJWKSet(new URL(config.jwks_uri)),
|
||||
};
|
||||
issuerCache.set(issuer, ctx);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/* Pure helper: true when the token's claims map to the configured admin group/role.
|
||||
Handles standard OIDC top-level `groups`/`roles` claims plus Keycloak's nested
|
||||
realm_access / resource_access role shapes (mirrors what KeycloakAuth.js does
|
||||
client-side). */
|
||||
function deriveIsAdmin(claims, settings) {
|
||||
if (!claims) return false;
|
||||
const groups = Array.isArray(claims.groups) ? [...claims.groups] : [];
|
||||
const roles = Array.isArray(claims.roles) ? [...claims.roles] : [];
|
||||
|
||||
if (settings.kind === 'keycloak') {
|
||||
const realmRoles = claims.realm_access && claims.realm_access.roles;
|
||||
if (Array.isArray(realmRoles)) roles.push(...realmRoles);
|
||||
const clientRoles = claims.resource_access
|
||||
&& claims.resource_access[settings.clientId]
|
||||
&& claims.resource_access[settings.clientId].roles;
|
||||
if (Array.isArray(clientRoles)) roles.push(...clientRoles);
|
||||
}
|
||||
|
||||
const { adminGroup, adminRole } = settings;
|
||||
if (adminGroup && groups.includes(adminGroup)) return true;
|
||||
if (adminRole && roles.includes(adminRole)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Connect middleware factory. Verifies Bearer id_token; sets req.auth on success. */
|
||||
function createOidcMiddleware(settings) {
|
||||
return async (req, res, next) => {
|
||||
const header = req.headers.authorization || '';
|
||||
const match = header.match(/^Bearer\s+(.+)$/i);
|
||||
if (!match) return next(); // Permissive: no token attached, let downstream gates decide
|
||||
const token = match[1].trim();
|
||||
if (!token) return next();
|
||||
|
||||
try {
|
||||
const { canonicalIssuer, jwks } = await getIssuerContext(settings.issuer);
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: canonicalIssuer,
|
||||
audience: settings.clientId,
|
||||
clockTolerance: '30s',
|
||||
});
|
||||
req.auth = {
|
||||
user: payload.preferred_username || payload.email || payload.sub || 'unknown',
|
||||
isAdmin: deriveIsAdmin(payload, settings),
|
||||
claims: payload,
|
||||
};
|
||||
return next();
|
||||
} catch (e) {
|
||||
console.warn('[auth-oidc] token verification failed:', e.message || e); // eslint-disable-line no-console
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Unauthorized - Invalid or expired token',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadOidcSettings,
|
||||
createOidcMiddleware,
|
||||
deriveIsAdmin,
|
||||
};
|
||||
26
src/App.vue
26
src/App.vue
@@ -183,17 +183,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/* Case-insensitive lookup that returns the canonical locale code, or undefined */
|
||||
resolveLocale(availibleLocales, userLang) {
|
||||
if (!userLang) return undefined;
|
||||
const target = userLang.toLowerCase();
|
||||
return availibleLocales.find((lang) => lang.toLowerCase() === target);
|
||||
},
|
||||
|
||||
/* Auto-detects users language from browser/ os, when not specified */
|
||||
autoDetectLanguage(availibleLocales) {
|
||||
const isLangSupported = (languageList, userLang) => languageList
|
||||
.map(lang => lang.toLowerCase()).find((lang) => lang === userLang.toLowerCase());
|
||||
|
||||
const usersBorwserLang1 = window.navigator.language || ''; // e.g. en-GB or or ''
|
||||
const usersBorwserLang2 = usersBorwserLang1.split('-')[0]; // e.g. en or undefined
|
||||
const usersSpairLangs = window.navigator.languages; // e.g [en, en-GB, en-US]
|
||||
return isLangSupported(availibleLocales, usersBorwserLang1)
|
||||
|| isLangSupported(availibleLocales, usersBorwserLang2)
|
||||
|| usersSpairLangs.find((spair) => isLangSupported(availibleLocales, spair))
|
||||
const usersSpairLangs = window.navigator.languages || []; // e.g [en, en-GB, en-US]
|
||||
return this.resolveLocale(availibleLocales, usersBorwserLang1)
|
||||
|| this.resolveLocale(availibleLocales, usersBorwserLang2)
|
||||
|| usersSpairLangs.map((spair) => this.resolveLocale(availibleLocales, spair)).find(Boolean)
|
||||
|| defaultLanguage;
|
||||
},
|
||||
|
||||
@@ -202,11 +206,9 @@ export default {
|
||||
const availibleLocales = this.$i18n.availableLocales; // All available locales
|
||||
const usersLang = localStorage[localStorageKeys.LANGUAGE] || this.appConfig.language;
|
||||
if (usersLang) {
|
||||
if (availibleLocales.includes(usersLang)) {
|
||||
return usersLang;
|
||||
} else {
|
||||
ErrorHandler(`Unsupported Language: '${usersLang}'`);
|
||||
}
|
||||
const resolved = this.resolveLocale(availibleLocales, usersLang);
|
||||
if (resolved) return resolved;
|
||||
ErrorHandler(`Unsupported Language: '${usersLang}'`);
|
||||
}
|
||||
return this.autoDetectLanguage(availibleLocales);
|
||||
},
|
||||
|
||||
@@ -47,12 +47,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "معلومات التطبيق",
|
||||
"error-log": "سجل الأخطاء",
|
||||
"no-errors": "لم يتم اكتشاف أخطاء مؤخراً",
|
||||
"help-support": "المساعدة والدعم",
|
||||
"help-support-description": "للحصول على دعم في تشغيل أو إعداد Dashy، راجع",
|
||||
"help-support-discussions": "المناقشات",
|
||||
"support-dashy-link": "صفحة المساهمات",
|
||||
"report-bug": "الإبلاغ عن خطأ",
|
||||
"report-bug-description": "إذا كنت تعتقد أنك وجدت خطأ، يرجى",
|
||||
"report-bug-link": "فتح Issue",
|
||||
@@ -68,8 +62,6 @@
|
||||
"privacy-and-security-security-policy": "سياسة الأمن",
|
||||
"license": "الترخيص",
|
||||
"license-under": "مرخص بموجب",
|
||||
"licence-third-party": "لتراخيص الوحدات الخارجية، يرجى الاطلاع على",
|
||||
"licence-third-party-link": "قانوني",
|
||||
"list-contributors": "للقائمة الكاملة للمساهمين والشكر، انظر",
|
||||
"list-contributors-link": "الاعتمادات",
|
||||
"version": "الإصدار"
|
||||
@@ -95,8 +87,6 @@
|
||||
"reset-config-msg-l2": "يجب عليك أولاً الاحتفاظ بنسخة احتياطية من أي تغييرات أجريتها محليًا ، إذا كنت ترغب في استخدامها في المستقبل.",
|
||||
"reset-config-msg-l3": "هل أنت متأكد انك تريد المتابعة؟",
|
||||
"data-cleared-msg": "تم مسح البيانات بنجاح",
|
||||
"actions-label": "أجراءات",
|
||||
"copy-config-label": "نسخ التكوين",
|
||||
"data-copied-msg": "تم نسخ التكوين إلى الحافظة",
|
||||
"reset-config-label": "إعادة التهيئة",
|
||||
"css-save-btn": "حفظ التغييرات",
|
||||
@@ -130,8 +120,7 @@
|
||||
"sign-out-tooltip": "خروج",
|
||||
"sign-in-tooltip": "تسجيل دخول",
|
||||
"sign-in-welcome": "مرحبًا {username}!",
|
||||
"hide": "إخفاء",
|
||||
"open": "فتح"
|
||||
"hide": "إخفاء"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "ملاحظة نسخة التطبيق",
|
||||
@@ -159,23 +148,11 @@
|
||||
"reset-toast": "تمت إزالة الألوان المخصصة لـ {theme}"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "حفظ الموقع",
|
||||
"location-local-label": "تطبيق محليا",
|
||||
"location-disk-label": "اكتب التغييرات في ملف التكوين",
|
||||
"save-button": "حفظ التغييرات",
|
||||
"preview-button": "معاينة التغييرات",
|
||||
"valid-label": "التكوين صالح",
|
||||
"status-success-msg": "اكتملت المهمة",
|
||||
"status-fail-msg": "فشلت المهمة",
|
||||
"success-msg-disk": "تمت كتابة ملف التكوين على القرص بنجاح",
|
||||
"success-msg-local": "تم حفظ التغييرات المحلية بنجاح",
|
||||
"success-note-l1": "يجب إعادة إنشاء التطبيق تلقائيًا.",
|
||||
"success-note-l2": "قد يستغرق هذا ما يصل إلى دقيقة.",
|
||||
"success-note-l3": "ستحتاج إلى تحديث الصفحة لتصبح التغييرات سارية المفعول.",
|
||||
"error-msg-save-mode": "الرجاء تحديد \"وضع الحفظ\": محلي أو ملف",
|
||||
"error-msg-cannot-save": "حدث خطأ أثناء حفظ التكوين",
|
||||
"error-msg-bad-json": "خطأ في JSON ، ربما يكون غير صحيح",
|
||||
"warning-msg-validation": "تحذير التحقق",
|
||||
"not-admin-note": "لا يمكنك الكتابة التغيير إلى القرص ، لأنك لم تقم بتسجيل الدخول كمسؤول"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -185,7 +162,6 @@
|
||||
"intro-l3": "لمزيد من المعلومات ، يرجى الاطلاع على",
|
||||
"intro-docs": "الوثائق",
|
||||
"backup-title-setup": "أصنع نسخة إحتياطية",
|
||||
"backup-title-update": "تحديث النسخ الاحتياطي",
|
||||
"password-label-setup": "اختر كلمة مرور",
|
||||
"password-label-update": "ادخل رقمك السري",
|
||||
"backup-button-setup": "دعم",
|
||||
@@ -202,17 +178,6 @@
|
||||
"backup-success-msg": "تم بنجاح",
|
||||
"restore-success-msg": "تمت استعادة التكوين بنجاح"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "فتح في",
|
||||
"sametab": "فتح في علامة التبويب الحالية",
|
||||
"newtab": "فتح في علامة تبويب جديدة",
|
||||
"modal": "فتح في Pop-Up Modal",
|
||||
"workspace": "فتح في عرض مساحة العمل",
|
||||
"options-section-title": "خيارات",
|
||||
"edit-item": "تعديل",
|
||||
"move-item": "نسخ أو نقل",
|
||||
"remove-item": "إزالة"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "فتح في",
|
||||
@@ -232,16 +197,9 @@
|
||||
"open-section": "فتح القسم",
|
||||
"edit-section": "تعديل",
|
||||
"expand-collapse": "توسيع / طي",
|
||||
"move-section": "نقل إلى",
|
||||
"remove-section": "إزالة"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"dev-by": "تطوير بواسطة",
|
||||
"licensed-under": "مرخص بموجب",
|
||||
"get-the": "احصل على",
|
||||
"source-code": "كود المصدر"
|
||||
},
|
||||
"interactive-editor": {
|
||||
"menu": {
|
||||
"start-editing-tooltip": "الدخول إلى المحرر التفاعلي",
|
||||
@@ -289,7 +247,6 @@
|
||||
"warning-msg-l3": "لتجنب العواقب غير المقصودة."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "تصدير الإعدادات",
|
||||
"copy-clipboard-btn": "نسخ إلى الحافظة",
|
||||
"copy-clipboard-tooltip": "نسخ جميع إعدادات التطبيق إلى حافظة النظام بتنسيق YAML",
|
||||
"download-file-btn": "تحميل كملف",
|
||||
@@ -306,7 +263,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "جاري التحميل...",
|
||||
"show-more": "توسيع التفاصيل",
|
||||
"cpu-details": "تفاصيل الـ CPU",
|
||||
"mem-details": "تفاصيل الذاكرة (RAM)",
|
||||
@@ -347,13 +303,9 @@
|
||||
"good-service-rest": "خدمة جيدة في جميع الخطوط الأخرى"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "تحميل",
|
||||
"upload": "رفع",
|
||||
"downloaded": "تم تحميله",
|
||||
"uploaded": "تم رفعه",
|
||||
"remaining": "متبقي",
|
||||
"up": "أعلى",
|
||||
"down": "أسفل"
|
||||
"remaining": "متبقي"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "عنوان VPN IP",
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
"reset-config-msg-l2": "Първо трябва да архивирате всички промени, които сте направили локално, ако искате да ги използвате в бъдеще.",
|
||||
"reset-config-msg-l3": "Искате ли да продължите?",
|
||||
"data-cleared-msg": "Данните са изчистени успешно",
|
||||
"actions-label": "Действия",
|
||||
"copy-config-label": "Копиране на конфиг",
|
||||
"data-copied-msg": "Конфигурацията беше копирана в clipboard",
|
||||
"reset-config-label": "Нулиране на конфигурация",
|
||||
"css-save-btn": "Запазване на промените",
|
||||
@@ -110,23 +108,11 @@
|
||||
"reset-toast": "Персонализираните цветове за {theme} бяха премахнати"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Запазване локално",
|
||||
"location-local-label": "Прилагане локално",
|
||||
"location-disk-label": "Запис на промените в конфигурационния файл",
|
||||
"save-button": "Запазване на промените",
|
||||
"preview-button": "Преглед на промените",
|
||||
"valid-label": "Валидни конфигурации",
|
||||
"status-success-msg": "Задачата завършена",
|
||||
"status-fail-msg": "Заадачата пропадна",
|
||||
"success-msg-disk": "Конфигурационният файл е записан на диска успешно",
|
||||
"success-msg-local": "Локалните промени са запазени успешно",
|
||||
"success-note-l1": "Приложението би трябвало да се ребилдне автоматично.",
|
||||
"success-note-l2": "Това може да отнеме до минута.",
|
||||
"success-note-l3": "Ще трябва да презаредите страницата за да бъдат приложени промените.",
|
||||
"error-msg-save-mode": "Моля изберете тип на запазване: Локално или Файл",
|
||||
"error-msg-cannot-save": "Появи се грешка при запис на конфигурация",
|
||||
"error-msg-bad-json": "Грешка с JSON, веротяно е неправилно форматиран",
|
||||
"warning-msg-validation": "Предупреждение за валидиране",
|
||||
"not-admin-note": "Не можете да пишете променено на диска, защото не сте влезли като администратор"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -135,7 +121,6 @@
|
||||
"intro-l2": "Всички данни са напълно криптирани от край до край с AES, като се използва паролата ви като ключ.",
|
||||
"intro-l3": "За повече информация, моля, вижте",
|
||||
"backup-title-setup": "Направете резервно копие",
|
||||
"backup-title-update": "Актуализиране на бекъп",
|
||||
"password-label-setup": "Изберете парола",
|
||||
"password-label-update": "Въведете Вашата парола",
|
||||
"backup-button-setup": "Бекъп",
|
||||
@@ -152,17 +137,6 @@
|
||||
"backup-success-msg": "Приключи с успех",
|
||||
"restore-success-msg": "Конфигурацията е възстановена успешно"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Отваряне в",
|
||||
"sametab": "Текущ таб",
|
||||
"newtab": "Нов таб",
|
||||
"modal": "Изскачащ модален прозорец",
|
||||
"workspace": "Изглед на работното пространство",
|
||||
"options-section-title": "Опции",
|
||||
"edit-item": "Редактиране",
|
||||
"move-item": "Копиране или изместване",
|
||||
"remove-item": "Премахване"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Отваряне в",
|
||||
@@ -181,7 +155,6 @@
|
||||
"open-section": "Отваряне на секция",
|
||||
"edit-section": "Редактиране",
|
||||
"expand-collapse": "Разгъване / свиване",
|
||||
"move-section": "Местене в",
|
||||
"remove-section": "Премахване"
|
||||
}
|
||||
},
|
||||
@@ -232,7 +205,6 @@
|
||||
"warning-msg-l3": "за да се избегнат непредвидени последици."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Експорт на конфигурация",
|
||||
"copy-clipboard-btn": "Копиране в клипборда",
|
||||
"copy-clipboard-tooltip": "Кпиране на конфигурациите на приложението в YAML формат",
|
||||
"download-file-btn": "Запазване във файл",
|
||||
@@ -241,7 +213,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Зареждане...",
|
||||
"show-more": "Повече детайли",
|
||||
"show-less": "Покажи по-малко",
|
||||
"open-link": "Отваряне на линка"
|
||||
@@ -280,13 +251,9 @@
|
||||
"good-service-rest": "Добра работа на всички други линии"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Теглене",
|
||||
"upload": "Качване",
|
||||
"downloaded": "Изтеглено",
|
||||
"uploaded": "Качено",
|
||||
"remaining": "Оставащо",
|
||||
"up": "Нагоре",
|
||||
"down": "Надолу"
|
||||
"remaining": "Оставащо"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,6 @@
|
||||
"reset-config-msg-l2": "আপনি যদি ভবিষ্যতে সেগুলি ব্যবহার করতে চান তবে প্রথমে স্থানীয়ভাবে আপনার করা যেকোনো পরিবর্তনের ব্যাকআপ নেওয়া উচিত।",
|
||||
"reset-config-msg-l3": "আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?",
|
||||
"data-cleared-msg": "ডেটা সফলভাবে মুছে ফেলা হয়েছে৷",
|
||||
"actions-label": "ক্রিয়াকলাপ",
|
||||
"copy-config-label": "সজ্জিতকরণ অনুলিপি",
|
||||
"data-copied-msg": "কনফিগারেশনটি ক্লিপবোর্ডে অনুলিপি করা হয়েছে",
|
||||
"reset-config-label": "সজ্জিতকরণ পুন:স্থাপন",
|
||||
"css-save-btn": "পরিবর্তনগুলোর সংরক্ষন",
|
||||
@@ -118,23 +116,11 @@
|
||||
"reset-toast": "{theme}-এর জন্য কাস্টম রং সরানো হয়েছে"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "স্থান সংরক্ষন",
|
||||
"location-local-label": "স্থানীয়ভাবে আবেদন করুন",
|
||||
"location-disk-label": "কনফিগ ফাইলে পরিবর্তন লিখুন",
|
||||
"save-button": "পরিবর্তনগুলোর সংরক্ষন",
|
||||
"preview-button": "পূর্বরূপ পরিবর্তন",
|
||||
"valid-label": "কনফিগ বৈধ",
|
||||
"status-success-msg": "কাজ সম্পূর্ণ",
|
||||
"status-fail-msg": "কাজ ব্যর্থ হয়েছে৷",
|
||||
"success-msg-disk": "কনফিগ ফাইল সফলভাবে ডিস্কে লেখা হয়েছে",
|
||||
"success-msg-local": "স্থানীয় পরিবর্তনগুলি সফলভাবে সংরক্ষিত হয়েছে৷",
|
||||
"success-note-l1": "অ্যাপটি স্বয়ংক্রিয়ভাবে পুনর্নির্মাণ করা উচিত।",
|
||||
"success-note-l2": "এতে এক মিনিট পর্যন্ত সময় লাগতে পারে।",
|
||||
"success-note-l3": "পরিবর্তনগুলি কার্যকর করার জন্য আপনাকে পৃষ্ঠাটি রিফ্রেশ করতে হবে৷",
|
||||
"error-msg-save-mode": "অনুগ্রহ করে একটি সংরক্ষণ মোড নির্বাচন করুন: স্থানীয় বা ফাইল।",
|
||||
"error-msg-cannot-save": "কনফিগার সংরক্ষণ করার সময় একটি ত্রুটি ঘটেছে৷",
|
||||
"error-msg-bad-json": "JSON-এ ত্রুটি, সম্ভবত বিকৃত",
|
||||
"warning-msg-validation": "বৈধতা সতর্কতা",
|
||||
"not-admin-note": "আপনি ডিস্কে পরিবর্তিত লিখতে পারবেন না, কারণ আপনি অ্যাডমিন হিসাবে লগ ইন করেননি৷"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -143,7 +129,6 @@
|
||||
"intro-l2": "All data is fully end-to-end encrypted with AES, using your password as the key.",
|
||||
"intro-l3": "আরো তথ্যের জন্য, দেখুন",
|
||||
"backup-title-setup": "একটি ব্যাকআপ করুন",
|
||||
"backup-title-update": "ব্যাকআপ আপডেট করুন",
|
||||
"password-label-setup": "একটি পাসওয়ার্ড চয়ন করুন",
|
||||
"password-label-update": "আপনার পাসওয়ার্ড লিখুন",
|
||||
"backup-button-setup": "ব্যাকআপ",
|
||||
@@ -160,17 +145,6 @@
|
||||
"backup-success-msg": "সফলভাবে সম্পন্ন",
|
||||
"restore-success-msg": "কনফিগারেশন সফলভাবে পুনরুদ্ধার করা হয়েছে"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "খুলুন",
|
||||
"sametab": "বর্তমান ট্যাব",
|
||||
"newtab": "নতুন ট্যাব",
|
||||
"modal": "পপ-আপ মডেল",
|
||||
"workspace": "ওয়ার্কস্পেস ভিউ",
|
||||
"options-section-title": "বিকল্প",
|
||||
"edit-item": "সম্পাদনা করুন",
|
||||
"move-item": "অনুলিপি বা সরান",
|
||||
"remove-item": "মুছুন"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "খুলুন",
|
||||
@@ -189,7 +163,6 @@
|
||||
"open-section": "বিভাগ খুলুন",
|
||||
"edit-section": "সম্পাদনা করুন",
|
||||
"expand-collapse": "প্রসারিত/সংকোচন করুন",
|
||||
"move-section": "সরান-",
|
||||
"remove-section": "মুছুন"
|
||||
}
|
||||
},
|
||||
@@ -240,7 +213,6 @@
|
||||
"warning-msg-l3": "অনাকাঙ্ক্ষিত পরিণতি এড়াতে।"
|
||||
},
|
||||
"export": {
|
||||
"export-title": "এক্সপোর্ট কনফিগার",
|
||||
"copy-clipboard-btn": "ক্লিপবোর্ডে কপি করুন",
|
||||
"copy-clipboard-tooltip": "YAML ফর্ম্যাটে, সিস্টেম ক্লিপবোর্ডে সমস্ত অ্যাপ কনফিগারেশন কপি করুন",
|
||||
"download-file-btn": "ফাইল হিসাবে ডাউনলোড করুন",
|
||||
@@ -249,7 +221,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "লোড হচ্ছে...",
|
||||
"show-more": "বিস্তারিত বিবরণ...",
|
||||
"show-less": "কম প্রদর্শন",
|
||||
"open-link": "পড়া চালিয়ে যান"
|
||||
@@ -288,13 +259,9 @@
|
||||
"good-service-rest": "অন্যান্য সমস্ত লাইনে ভাল পরিষেবা"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "ডাউনলোড করুন",
|
||||
"upload": "আপলোড করুন",
|
||||
"downloaded": "ডাউনলোড করা হয়েছে",
|
||||
"uploaded": "আপলোড করা হয়েছে",
|
||||
"remaining": "অবশিষ্ট",
|
||||
"up": "উপরে",
|
||||
"down": "নিচে"
|
||||
"remaining": "অবশিষ্ট"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "ভিপিএন আইপি",
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
"reset-config-msg-l2": "Všechny změny, které jste vykonali lokálně, byste si měli co nejdříve zálohovat, pokud je chcete použít v budoucnosti.",
|
||||
"reset-config-msg-l3": "Opravdu chcete pokračovat?",
|
||||
"data-cleared-msg": "Údaje byly úspěšne vymazány",
|
||||
"actions-label": "Akce",
|
||||
"copy-config-label": "Kopírovat konfiguraci",
|
||||
"data-copied-msg": "Konfigurace byla zkopírována do schránky",
|
||||
"reset-config-label": "Obnovit konfiguraci",
|
||||
"css-save-btn": "Uložit změny",
|
||||
@@ -110,23 +108,11 @@
|
||||
"reset-toast": "Vlastní barvy pro {theme} byly odstraněny"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Uložit umístění",
|
||||
"location-local-label": "Aplikovať lokálně",
|
||||
"location-disk-label": "Zapsat změny do konfiguračního souboru",
|
||||
"save-button": "Uložit změny",
|
||||
"preview-button": "Ukázka změn",
|
||||
"valid-label": "Konfigurace je platná",
|
||||
"status-success-msg": "Akce dokončena",
|
||||
"status-fail-msg": "Akce selhala",
|
||||
"success-msg-disk": "Konfigurační soubor byl úspěšně zapsán na disk",
|
||||
"success-msg-local": "Místní změny byly úspěšne uloženy",
|
||||
"success-note-l1": "Aplikace by se měla automaticky obnovit.",
|
||||
"success-note-l2": "Můýe to trvat až minutu.",
|
||||
"success-note-l3": "Aby se změny projevily, budete museť obnovit stránku.",
|
||||
"error-msg-save-mode": "Vyberte režim uložení: Místní/Soubor",
|
||||
"error-msg-cannot-save": "Při ukládání konfigurace se vyskytla chyba",
|
||||
"error-msg-bad-json": "Chyba v JSON, možná má nesprávny formát",
|
||||
"warning-msg-validation": "Chyba validace",
|
||||
"not-admin-note": "Nemůžete zapisovat změny na disk, protože nejste přihlášený/á jako správca"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -135,7 +121,6 @@
|
||||
"intro-l2": "Všechny údaje jsou end-to-end šifrované pomocí AES, pričemž jako klíč se používá Vaše heslo.",
|
||||
"intro-l3": "Více informací nájdete na",
|
||||
"backup-title-setup": "Vytvořit zálohu",
|
||||
"backup-title-update": "Aktualizovat zálohu",
|
||||
"password-label-setup": "Vytvořte heslo pro cloud",
|
||||
"password-label-update": "Zadajte heslo pro cloud",
|
||||
"backup-button-setup": "Záloha",
|
||||
@@ -152,17 +137,6 @@
|
||||
"backup-success-msg": "Záloha úspěšně dokončena.",
|
||||
"restore-success-msg": "Záloha byla úspěšně obnovena"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Otevřít v",
|
||||
"sametab": "Aktuální karta",
|
||||
"newtab": "Nová karta",
|
||||
"modal": "Modální okno",
|
||||
"workspace": "Zobrazení pracovního prostoru",
|
||||
"options-section-title": "Možnosti",
|
||||
"edit-item": "Upravit",
|
||||
"move-item": "Kopírovat nebo přesunout",
|
||||
"remove-item": "Odstranit"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Otevřít v",
|
||||
@@ -181,7 +155,6 @@
|
||||
"open-section": "Otevřená sekce",
|
||||
"edit-section": "Upravit",
|
||||
"expand-collapse": "Rozbalit / sbaliť",
|
||||
"move-section": "Přesunout do",
|
||||
"remove-section": "Odstranit"
|
||||
}
|
||||
},
|
||||
@@ -232,7 +205,6 @@
|
||||
"warning-msg-l3": "Aby se předešlo neúmyslným následkům."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exportovat konfiguraci",
|
||||
"copy-clipboard-btn": "Kopírovať do schránky",
|
||||
"copy-clipboard-tooltip": "Kopírovať všechny konfigurace aplikace do systémové schránky ve formátu YAML",
|
||||
"download-file-btn": "Stáhnout jako soubor",
|
||||
@@ -241,7 +213,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Načítání...",
|
||||
"show-more": "Rozbalit podrobnosti",
|
||||
"show-less": "Zobrazit méně",
|
||||
"open-link": "Pokračovať ve čtení"
|
||||
@@ -280,13 +251,9 @@
|
||||
"good-service-rest": "Dobré služby na všech ostatních linkách"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Stáhnout",
|
||||
"upload": "Nahrat",
|
||||
"downloaded": "Stáhnuto",
|
||||
"uploaded": "Nahráno",
|
||||
"remaining": "Zbývající",
|
||||
"up": "Aktivní",
|
||||
"down": "Neaktivní"
|
||||
"remaining": "Zbývající"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "IP VPN",
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
"reset-config-msg-l2": "Du bør først sikkerhedskopiere eventuelle ændringer, du har foretaget lokalt, hvis du vil bruge dem i fremtiden.",
|
||||
"reset-config-msg-l3": "Er du sikker på at du vil fortsætte?",
|
||||
"data-cleared-msg": "Data blev succesfuldt ryddet",
|
||||
"actions-label": "Handlinger",
|
||||
"copy-config-label": "Kopiere Config",
|
||||
"data-copied-msg": "Konfigurationen er blevet kopieret til clipboard",
|
||||
"reset-config-label": "Nulstil konfiguration",
|
||||
"css-save-btn": "Gem ændringer",
|
||||
@@ -110,23 +108,11 @@
|
||||
"reset-toast": "Tilpassede farver for {theme} fjernet"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Lokation for gem",
|
||||
"location-local-label": "Gem lokalt",
|
||||
"location-disk-label": "Skriv ændringer til konfigurationsfil",
|
||||
"save-button": "Gem ændringer",
|
||||
"preview-button": "Forhåndsvisning af ændringer",
|
||||
"valid-label": "Konfigurationen er gyldig",
|
||||
"status-success-msg": "Opgave fuldført",
|
||||
"status-fail-msg": "Opgave mislykkedes",
|
||||
"success-msg-disk": "Konfigurationsfilen er skrevet til disken",
|
||||
"success-msg-local": "Lokale ændringer blev gemt",
|
||||
"success-note-l1": "Appen skal genopbygges automatisk.",
|
||||
"success-note-l2": "Dette kan tage op til et minut.",
|
||||
"success-note-l3": "Du skal opdatere siden for at ændringerne træder i kraft.",
|
||||
"error-msg-save-mode": "Vælg venligst en gemtilstand: Lokal eller Fil",
|
||||
"error-msg-cannot-save": "Der opstod en fejl under lagring af konfiguration",
|
||||
"error-msg-bad-json": "Fejl i JSON, muligvis forkert udformet",
|
||||
"warning-msg-validation": "Valideringsadvarsel",
|
||||
"not-admin-note": "Du kan ikke skrive ændringer til disk, fordi du ikke er logget ind som admin"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -135,7 +121,6 @@
|
||||
"intro-l2": "Alle data er fuldstændigt end-to-end krypteret med AES, ved at bruge din adgangskode som nøglen.",
|
||||
"intro-l3": "For mere information, se venligst",
|
||||
"backup-title-setup": "Lav en sikkerhedskopi",
|
||||
"backup-title-update": "Opdater sikkerhedskopiering",
|
||||
"password-label-setup": "Vælg et kodeord",
|
||||
"password-label-update": "Skriv dit kodeord",
|
||||
"backup-button-setup": "Sikkerhedskopiering",
|
||||
@@ -152,17 +137,6 @@
|
||||
"backup-success-msg": "Afsluttet med succes",
|
||||
"restore-success-msg": "Konfiguration gendannet med succes"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Åbn i",
|
||||
"sametab": "Aktuel fane",
|
||||
"newtab": "Ny fane",
|
||||
"modal": "Pop-Up Modal",
|
||||
"workspace": "Arbejdsrumsvisning",
|
||||
"options-section-title": "Muligheder",
|
||||
"edit-item": "Rediger",
|
||||
"move-item": "Kopier eller flyt",
|
||||
"remove-item": "Fjern"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Åbn i",
|
||||
@@ -181,7 +155,6 @@
|
||||
"open-section": "Åbn sektion",
|
||||
"edit-section": "Rediger",
|
||||
"expand-collapse": "Udvid / Skjul",
|
||||
"move-section": "Flytte til",
|
||||
"remove-section": "Fjerne"
|
||||
}
|
||||
},
|
||||
@@ -232,7 +205,6 @@
|
||||
"warning-msg-l3": "for at undgå utilsigtede konsekvenser."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Eksporter konfiguration",
|
||||
"copy-clipboard-btn": "Kopier til udklipsholder",
|
||||
"copy-clipboard-tooltip": "Kopier alle app-konfigurationer til systemets udklipsholder i YAML-format",
|
||||
"download-file-btn": "Download som fil",
|
||||
@@ -241,7 +213,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Indlæser...",
|
||||
"show-more": "Udvid detaljer",
|
||||
"show-less": "Vis mindre",
|
||||
"open-link": "Fortsæt med at læse"
|
||||
@@ -280,13 +251,9 @@
|
||||
"good-service-rest": "God service på alle andre linjer"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"downloaded": "Downloaded",
|
||||
"uploaded": "Uploaded",
|
||||
"remaining": "Tilbage",
|
||||
"up": "Op",
|
||||
"down": "Ned"
|
||||
"remaining": "Tilbage"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "App Info",
|
||||
"support-dashy-link": "Contributions Seite",
|
||||
"report-bug": "Melde einen Fehler",
|
||||
"report-bug-description": "Wenn Sie glauben, einen Fehler gefunden zu haben, dann bitte",
|
||||
"report-bug-link": "öffne ein Issue",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "Security Policy",
|
||||
"license": "Lizenz",
|
||||
"license-under": "Lizensiert unter",
|
||||
"licence-third-party": "Für Lizenzen von Drittanbietermodulen, siehe",
|
||||
"licence-third-party-link": "Legal",
|
||||
"list-contributors": "Für eine vollstandige Liste aller Beteiligten und Dank, siehe",
|
||||
"list-contributors-link": "Credits",
|
||||
"version": "Version"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Sie sollten zuerst alle Änderungen, die Sie lokal vorgenommen haben, sichern, wenn Sie sie in Zukunft wiederverwenden möchten.",
|
||||
"reset-config-msg-l3": "Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||
"data-cleared-msg": "Daten erfolgreich gelöscht",
|
||||
"actions-label": "Aktionen",
|
||||
"copy-config-label": "Konfiguration kopieren",
|
||||
"data-copied-msg": "Konfiguration wurde in die Zwischenablage kopiert",
|
||||
"reset-config-label": "Konfiguration zurücksetzen",
|
||||
"css-save-btn": "Änderungen speichern",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Abmelden",
|
||||
"sign-in-tooltip": "Anmelden",
|
||||
"sign-in-welcome": "Hallo {username}!",
|
||||
"hide": "Verstecke",
|
||||
"open": "Öffne"
|
||||
"hide": "Verstecke"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy Version",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "Benutzerdefinierte Farben für {theme} wurden entfernt"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Speicherort",
|
||||
"location-local-label": "Lokal anwenden",
|
||||
"location-disk-label": "Änderungen in die Konfigurationsdatei schreiben",
|
||||
"save-button": "Änderungen speichern",
|
||||
"preview-button": "Vorschau der Änderungen",
|
||||
"valid-label": "Syntax ist gültig",
|
||||
"status-success-msg": "Aufgabe abgeschlossen",
|
||||
"status-fail-msg": "Aufgabe fehlgeschlagen",
|
||||
"success-msg-disk": "Konfigurationsdatei wurde erfolgreich auf die Festplatte geschrieben",
|
||||
"success-msg-local": "Lokale Änderungen wurden erfolgreich gespeichert",
|
||||
"success-note-l1": "Die Applikation sollte automatisch re-kompiliert werden.",
|
||||
"success-note-l2": "Dies kann bis zu einer Minute dauern.",
|
||||
"success-note-l3": "Sie müssen die Seite aktualisieren damit die Änderungen wirksam werden.",
|
||||
"error-msg-save-mode": "Bitte wählen Sie einen Speichermodus: Lokal oder Datei",
|
||||
"error-msg-cannot-save": "Beim Speichern der Konfiguration ist ein Fehler aufgetreten",
|
||||
"error-msg-bad-json": "Fehler in JSON-Daten, möglicherweise fehlerhafter Syntax",
|
||||
"warning-msg-validation": "Validierungswarnung",
|
||||
"not-admin-note": "Änderungen können nicht auf die Festplatte gespeichert werden, da Sie nicht als Administrator angemeldet sind"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "Weitere Informationen finden Sie in der",
|
||||
"intro-docs": "Dokumentation",
|
||||
"backup-title-setup": "Backup erstellen",
|
||||
"backup-title-update": "Backup aktualisieren",
|
||||
"password-label-setup": "Passwort auswählen",
|
||||
"password-label-update": "Passwort eingeben",
|
||||
"backup-button-setup": "Backup",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Erfolgreich abgeschlossen",
|
||||
"restore-success-msg": "Konfiguration erfolgreich wiederhergestellt"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Öffne in",
|
||||
"sametab": "Aktueller Tab",
|
||||
"newtab": "Neuer Tab",
|
||||
"modal": "Popup Modal",
|
||||
"workspace": "Arbeitsflächenansicht",
|
||||
"options-section-title": "Optionen",
|
||||
"edit-item": "Bearbeiten",
|
||||
"move-item": "Kopieren oder Verschieben",
|
||||
"remove-item": "Entfernen"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Öffnen in",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Sektion öffnen",
|
||||
"edit-section": "Bearbeiten",
|
||||
"expand-collapse": "Aus- / Einklappen",
|
||||
"move-section": "Verschieben nach",
|
||||
"remove-section": "Entfernen"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "um unbeabsichtigte Folgen zu vermeiden."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Konfiguration exportieren",
|
||||
"copy-clipboard-btn": "In Zwischenablage kopieren",
|
||||
"copy-clipboard-tooltip": "Applikationskonfiguration als YAML in Zwischenablage kopieren",
|
||||
"download-file-btn": "Datei herunterladen",
|
||||
@@ -285,7 +253,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Lade...",
|
||||
"show-more": "Details",
|
||||
"cpu-details": "Details CPU",
|
||||
"mem-details": "Details Arbeitsspeicher",
|
||||
@@ -326,13 +293,9 @@
|
||||
"good-service-rest": "Guter Service auf allen anderen Leitungen"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"downloaded": "Heruntergeladen",
|
||||
"uploaded": "Hochgeladen",
|
||||
"remaining": "Verbleibend",
|
||||
"up": "Hoch",
|
||||
"down": "Runter"
|
||||
"remaining": "Verbleibend"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Πληροφορίες Εφαρμογής",
|
||||
"support-dashy-link": "Σελίδα συνεισφορών",
|
||||
"report-bug": "Αναφέρετε Πρόβλημα",
|
||||
"report-bug-description": "Αν πιστεύετε ότι έχετε βρει κάποιο σφάλμα, τότε παρακαλείστε να",
|
||||
"report-bug-link": "υποβάλετε μια Αναφορά",
|
||||
@@ -56,14 +55,12 @@
|
||||
"privacy-and-security": "Απόρρητο & Ασφάλεια",
|
||||
"privacy-and-security-l1": "Για μια ανάλυση του τρόπου διαχείρισης των δεδομένων σας από τον Dashy, ανατρέξτε στην",
|
||||
"privacy-and-security-privacy-policy": "Πολιτική Απορρήτου",
|
||||
"app-info.privacy-and-security-advice": "Για συμβουλές σχετικά με την ασφάλεια του dashboard σας, μπορείτε να ανατρέξετε στα",
|
||||
"app-info.privacy-and-security-advice-link": "Εγγραφα Διαχείρισης",
|
||||
"privacy-and-security-advice": "Για συμβουλές σχετικά με την ασφάλεια του dashboard σας, μπορείτε να ανατρέξετε στα",
|
||||
"privacy-and-security-advice-link": "Εγγραφα Διαχείρισης",
|
||||
"privacy-and-security-security-issue": "Αν έχετε εντοπίσει κάποιο πιθανό ζήτημα ασφαλείας, αναφέρετέ το ακολουθώντας την",
|
||||
"privacy-and-security-security-policy": "Πολιτική Ασφαλείας",
|
||||
"license": "Αδεια",
|
||||
"license-under": "Άδεια χρήσης σύμφωνα με την",
|
||||
"licence-third-party": "Για άδειες χρήσης δομοστοιχείων λογισμικού τρίτων, ανατρέξτε στο",
|
||||
"licence-third-party-link": "Νομικό Πλαίσιο",
|
||||
"list-contributors": "Για την πλήρη λίστα των συντελεστών και ευχαριστιών, ανατρέξτε στις",
|
||||
"list-contributors-link": "Ευχαριστίες",
|
||||
"version": "Εκδοση"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Θα πρέπει πρώτα να δημιουργήσετε αντίγραφα ασφαλείας για τυχούσες αλλαγές που έχετε κάνει τοπικά, αν θέλετε να είναι διαθέσιμες για μελλοντική χρήση.",
|
||||
"reset-config-msg-l3": "Είστε βέβαιοι ότι θέλετε να συνεχίσετε;",
|
||||
"data-cleared-msg": "Τα δεδομένα εκκαθαρίστηκαν με επιτυχία",
|
||||
"actions-label": "Ενέργειες",
|
||||
"copy-config-label": "Αντιγραφή διαμόρφωσης",
|
||||
"data-copied-msg": "Η διαμόρφωση έχει αντιγραφεί στο πρόχειρο",
|
||||
"reset-config-label": "Επαναφορά Διαμόρφωσης",
|
||||
"css-save-btn": "Αποθήκευση Αλλαγών",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Αποσύνδεση",
|
||||
"sign-in-tooltip": "Σύνδεση",
|
||||
"sign-in-welcome": "Γεια σου {username}!",
|
||||
"hide": "Απόκρυψη",
|
||||
"open": "Ανοιγμα"
|
||||
"hide": "Απόκρυψη"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy έκδοση",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "Τα προσαρμοσμένα χρώματα για το {theme} καταργήθηκαν"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Αποθήκευση Τοποθεσίας",
|
||||
"location-local-label": "Εφαρμογή Τοπικά",
|
||||
"location-disk-label": "Εγγραφή Αλλαγών στο Αρχείο Διαμόρφωσης",
|
||||
"save-button": "Αποθήκευση Αλλαγών",
|
||||
"preview-button": "Προεπισκόπηση Αλλαγών",
|
||||
"valid-label": "Η Διαμόρφωση είναι Εγκυρη",
|
||||
"status-success-msg": "Η Εργασία Ολοκληρώθηκε",
|
||||
"status-fail-msg": "Η Εργασία Απέτυχε",
|
||||
"success-msg-disk": "Το αρχείο διαμόρφωσης εγγράφηκε στον δίσκο επιτυχώς",
|
||||
"success-msg-local": "Οι τοπικές αλλαγές αποθηκεύτηκαν με επιτυχία",
|
||||
"success-note-l1": "Η εφαρμογή θα πρέπει να χτιστεί εκ νέου, αυτομάτως.",
|
||||
"success-note-l2": "Αυτό μπορεί να διαρκέσει έως και ένα λεπτό.",
|
||||
"success-note-l3": "Θα χρειαστεί να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές.",
|
||||
"error-msg-save-mode": "Παρακαλώ επιλέξτε έναν τρόπο αποθήκευσης: Τοπικά ή σε Αρχείο",
|
||||
"error-msg-cannot-save": "Παρουσιάστηκε σφάλμα κατά την αποθήκευση των παραμέτρων",
|
||||
"error-msg-bad-json": "Σφάλμα στο JSON: Μη έγκυρη μορφοποίηση",
|
||||
"warning-msg-validation": "Προειδοποίηση επικύρωσης",
|
||||
"not-admin-note": "Δεν μπορείτε να γράψετε αλλαγές στον δίσκο, επειδή δεν έχετε συνδεθεί ως διαχειριστής"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "Για περισσότερες πληροφορίες, ανατρέξτε στα",
|
||||
"intro-docs": "έγγραφα",
|
||||
"backup-title-setup": "Δημιουργία αντιγράφου ασφαλείας",
|
||||
"backup-title-update": "Ενημέρωση αντιγράφου ασφαλείας",
|
||||
"password-label-setup": "Επιλέξτε έναν κωδικό πρόσβασης",
|
||||
"password-label-update": "Εισάγετε τον κωδικό σας",
|
||||
"backup-button-setup": "Αντίγραφο Ασφαλείας",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Ολοκληρώθηκε Επιτυχώς",
|
||||
"restore-success-msg": "Η Διαμόρφωση Αποκαταστάθηκε Επιτυχώς"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Ανοιγμα σε",
|
||||
"sametab": "Τρέχουσα Καρτέλα",
|
||||
"newtab": "Νέα Καρτέλα",
|
||||
"modal": "Αναδυόμενο Παράθυρο",
|
||||
"workspace": "Προβολή Χώρου Εργασίας",
|
||||
"options-section-title": "Επιλογές",
|
||||
"edit-item": "Επεξεργασία",
|
||||
"move-item": "Αντιγραφή ή Μετακίνηση",
|
||||
"remove-item": "Αφαίρεση"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Ανοιγμα Σε",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Ανοιγμα ενότητας",
|
||||
"edit-section": "Επεξεργασία",
|
||||
"expand-collapse": "Ανάπτυξη / Σύμπτυξη",
|
||||
"move-section": "Μετακίνηση σε",
|
||||
"remove-section": "Αφαίρεση"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "προς αποφυγή ανεπιθύμητων συνεπειών."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Εξαγωγή διαμόρφωσης",
|
||||
"copy-clipboard-btn": "Αντιγραφή στο πρόχειρο",
|
||||
"copy-clipboard-tooltip": "Αντιγράψτε όλες τις ρυθμίσεις της εφαρμογής στο πρόχειρο του συστήματος, σε YAML μορφή",
|
||||
"download-file-btn": "Λήψη ως Αρχείο",
|
||||
@@ -285,7 +253,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Γίνεται φόρτωση...",
|
||||
"show-more": "Ανάπτυξη Λεπτομερειών",
|
||||
"show-less": "Εμφάνιση Λιγότερων",
|
||||
"open-link": "Συνέχιση Ανάγνωσης"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"app-info": {
|
||||
"license": "Licence",
|
||||
"licence-third-party": "For licences for third-party modules, please see"
|
||||
"license": "Licence"
|
||||
},
|
||||
"theme-maker": {
|
||||
"reset-toast": "Custom Colours for {theme} Removed"
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "App Info",
|
||||
"support-dashy-link": "Contributions page",
|
||||
"sponsor-heading": "Sponsor Dashy",
|
||||
"sponsor-l1-prefix": "Enjoying Dashy? Consider",
|
||||
"sponsor-l1-link": "sponsoring me on GitHub",
|
||||
@@ -74,8 +73,6 @@
|
||||
"privacy-and-security-security-policy": "Security Policy",
|
||||
"license": "License",
|
||||
"license-under": "Licensed under",
|
||||
"licence-third-party": "For licenses for third-party modules, please see",
|
||||
"licence-third-party-link": "Legal",
|
||||
"list-contributors": "For the full list of contributors and thanks, see",
|
||||
"list-contributors-link": "Credits",
|
||||
"version": "Version"
|
||||
@@ -102,8 +99,6 @@
|
||||
"reset-config-msg-l2": "You should first backup any changes you've made locally, if you want to use them in the future.",
|
||||
"reset-config-msg-l3": "Are you sure you want to proceed?",
|
||||
"data-cleared-msg": "Data cleared successfully",
|
||||
"actions-label": "Actions",
|
||||
"copy-config-label": "Copy Config",
|
||||
"data-copied-msg": "Config has been copied to clipboard",
|
||||
"reset-config-label": "Reset Config",
|
||||
"css-save-btn": "Save Changes",
|
||||
@@ -162,7 +157,6 @@
|
||||
"sign-in-welcome": "Hello {username}!",
|
||||
"please-login": "Log in for full access",
|
||||
"hide": "Hide",
|
||||
"open": "Open",
|
||||
"options-title": "Options",
|
||||
"options-tooltip": "Options"
|
||||
},
|
||||
@@ -256,23 +250,11 @@
|
||||
"reset-toast": "Custom Colors for {theme} Removed"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Save Location",
|
||||
"location-local-label": "Apply Locally",
|
||||
"location-disk-label": "Write Changes to Config File",
|
||||
"save-button": "Save Changes",
|
||||
"preview-button": "Preview Changes",
|
||||
"valid-label": "Config is Valid",
|
||||
"status-success-msg": "Task Complete",
|
||||
"status-fail-msg": "Task Failed",
|
||||
"success-msg-disk": "Config file written to disk successfully",
|
||||
"success-msg-local": "Local changes saved successfully",
|
||||
"success-note-l1": "You will need to refresh the page for changes to take effect.",
|
||||
"success-note-l2": "",
|
||||
"success-note-l3": "",
|
||||
"error-msg-save-mode": "Please select a Save Mode: Local or File",
|
||||
"error-msg-cannot-save": "An error occurred saving config",
|
||||
"error-msg-bad-json": "Error in JSON, possibly malformed",
|
||||
"warning-msg-validation": "Validation Warning",
|
||||
"not-admin-note": "You cannot write changes to disk because you are not logged in as an administrator",
|
||||
"preview-applied-msg": "Config previewed. Close this modal to see the changes.",
|
||||
"reset-confirm-msg": "Discard all unsaved changes and revert to the original config?",
|
||||
@@ -301,7 +283,6 @@
|
||||
"intro-l3": "For more info, please see the",
|
||||
"intro-docs": "docs",
|
||||
"backup-title-setup": "Make a Backup",
|
||||
"backup-title-update": "Update Backup",
|
||||
"password-label-setup": "Choose a Password",
|
||||
"password-label-update": "Enter your Password",
|
||||
"backup-button-setup": "Backup",
|
||||
@@ -318,17 +299,6 @@
|
||||
"backup-success-msg": "Completed Successfully",
|
||||
"restore-success-msg": "Config Restored Successfully"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Open In",
|
||||
"sametab": "Current Tab",
|
||||
"newtab": "New Tab",
|
||||
"modal": "Pop-Up Modal",
|
||||
"workspace": "Workspace View",
|
||||
"options-section-title": "Options",
|
||||
"edit-item": "Edit",
|
||||
"move-item": "Copy or Move",
|
||||
"remove-item": "Remove"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Open In",
|
||||
@@ -348,7 +318,6 @@
|
||||
"open-section": "Open Section",
|
||||
"edit-section": "Edit",
|
||||
"expand-collapse": "Expand / Collapse",
|
||||
"move-section": "Move To",
|
||||
"remove-section": "Remove",
|
||||
"section-options": "Section Options"
|
||||
}
|
||||
@@ -423,7 +392,6 @@
|
||||
"warning-msg-l3": "to avoid unintended consequences."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Export Config",
|
||||
"copy-clipboard-btn": "Copy to Clipboard",
|
||||
"copy-clipboard-tooltip": "Copy all app config to system clipboard, in YAML format",
|
||||
"download-file-btn": "Download as File",
|
||||
@@ -461,7 +429,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Loading...",
|
||||
"show-more": "Expand Details",
|
||||
"cpu-details": "CPU Details",
|
||||
"mem-details": "Memory Details",
|
||||
@@ -502,13 +469,9 @@
|
||||
"good-service-rest": "Good Service on all other Lines"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"downloaded": "Downloaded",
|
||||
"uploaded": "Uploaded",
|
||||
"remaining": "Remaining",
|
||||
"up": "Up",
|
||||
"down": "Down"
|
||||
"remaining": "Remaining"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
"reset-config-msg-l2": "Es recomedable realizar primero una copia de seguridad de los cambios hecho en su configuración local, por si los necesitas usar en el futuro.",
|
||||
"reset-config-msg-l3": "¿Estás seguro de que quieres continuar?",
|
||||
"data-cleared-msg": "Datos eliminados correctamente",
|
||||
"actions-label": "Acciones",
|
||||
"copy-config-label": "Copiar la Configuración",
|
||||
"data-copied-msg": "La configuración ha sido copiada al portapapeles",
|
||||
"reset-config-label": "Resetear la Configuración",
|
||||
"css-save-btn": "Guardar Cambios",
|
||||
@@ -110,23 +108,11 @@
|
||||
"reset-toast": "Colores personalizados de {theme} borrados"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Guardar Ubicación",
|
||||
"location-local-label": "Aplicar localmente",
|
||||
"location-disk-label": "Guardar cambion en el fichero de configuración",
|
||||
"save-button": "Guardar Cambios",
|
||||
"preview-button": "Previsualizar los cambios",
|
||||
"valid-label": "La configuración es válida",
|
||||
"status-success-msg": "Tarea Completada",
|
||||
"status-fail-msg": "La Tarea ha fallado",
|
||||
"success-msg-disk": "Fichero de configuración guardado en disco correctamente",
|
||||
"success-msg-local": "Los cambios locales se han guardado correctamente",
|
||||
"success-note-l1": "La app se recompilará automáticamente.",
|
||||
"success-note-l2": "Esto puede llevar algo más de un minuto.",
|
||||
"success-note-l3": "Es necesario refrescar la página para que los cambios tengan efecto.",
|
||||
"error-msg-save-mode": "Por favor selecciona un modo de Guardar: Local o Fichero",
|
||||
"error-msg-cannot-save": "Se ha producido un error al guardar la configuración",
|
||||
"error-msg-bad-json": "Error en el JSON, probablemente esté mal construído",
|
||||
"warning-msg-validation": "Advertencia de validación",
|
||||
"not-admin-note": "No puedes guardar los cambios en el disco, porque no estás conectado como un Administrador"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -135,7 +121,6 @@
|
||||
"intro-l2": "Todos los datos están cifrados de extremo a extremo con AES, la Contraseña elegida es la clave de cifrado.",
|
||||
"intro-l3": "Para más información, por favor consulta",
|
||||
"backup-title-setup": "Hacer una copia de seguridad",
|
||||
"backup-title-update": "Restaurar copia de seguridad",
|
||||
"password-label-setup": "Selecciona una Contraseña",
|
||||
"password-label-update": "Escribe tu Contraseña",
|
||||
"backup-button-setup": "Copia de Seguridad",
|
||||
@@ -152,17 +137,6 @@
|
||||
"backup-success-msg": "Completado con éxito",
|
||||
"restore-success-msg": "Configuración restaurada con éxito"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Abrir en",
|
||||
"sametab": "Abrir en la pestaña actual",
|
||||
"newtab": "Abrir en una nueva pestaña",
|
||||
"modal": "Abrir en un Pop-Up",
|
||||
"workspace": "Abrir en el espacio de trabajo",
|
||||
"options-section-title": "Opciones",
|
||||
"edit-item": "Editar",
|
||||
"move-item": "Copiar o Mover",
|
||||
"remove-item": "Eliminar"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Abrir en",
|
||||
@@ -181,7 +155,6 @@
|
||||
"open-section": "Abrir la sección",
|
||||
"edit-section": "Editar",
|
||||
"expand-collapse": "Expandir / Colapsar",
|
||||
"move-section": "Mover a",
|
||||
"remove-section": "Eliminar"
|
||||
}
|
||||
},
|
||||
@@ -232,7 +205,6 @@
|
||||
"warning-msg-l3": "para evitar consecuencias imprevistas."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exportar configuración",
|
||||
"copy-clipboard-btn": "Copiar al portapapeles",
|
||||
"copy-clipboard-tooltip": "Copiar toda la configuración de la aplicación al portapapeles del sistema, en formato YAML",
|
||||
"download-file-btn": "Descargar como archivo",
|
||||
@@ -241,7 +213,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Cargando...",
|
||||
"show-more": "Ampliar detalles",
|
||||
"show-less": "Mostrar menos",
|
||||
"open-link": "Continuar leyendo"
|
||||
@@ -280,13 +251,9 @@
|
||||
"good-service-rest": "Buen servicio en todas las demás líneas"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Descargar",
|
||||
"upload": "Cargar",
|
||||
"downloaded": "Descargado",
|
||||
"uploaded": "Cargado",
|
||||
"remaining": "Restante",
|
||||
"up": "Levantado",
|
||||
"down": "Caído"
|
||||
"remaining": "Restante"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "IP de la VPN",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Informations sur l'application",
|
||||
"support-dashy-link": "« Contributions »",
|
||||
"report-bug": "Signaler un bug",
|
||||
"report-bug-description": "Si vous pensez avoir trouvé un bug, alors s'il vous plaît, ",
|
||||
"report-bug-link": "signalez-le",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "politique en matière de sécurité",
|
||||
"license": "Licence",
|
||||
"license-under": "Sous licence",
|
||||
"licence-third-party": "Pour les licences des modules tiers, veuillez consulter",
|
||||
"licence-third-party-link": "le document juridique",
|
||||
"list-contributors": "Pour la liste complète des contributeurs et les remerciements, voir",
|
||||
"list-contributors-link": "les crédits",
|
||||
"version": "Version"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Vous devez d'abord sauvegarder toutes les modifications locales si vous souhaitez les utiliser à l'avenir.",
|
||||
"reset-config-msg-l3": "Êtes-vous sûr de vouloir continuer ?",
|
||||
"data-cleared-msg": "Données effacées avec succès",
|
||||
"actions-label": "Actions",
|
||||
"copy-config-label": "Copier la configuration",
|
||||
"data-copied-msg": "La configuration a été copiée dans le presse-papiers",
|
||||
"reset-config-label": "Réinitialiser la configuration",
|
||||
"css-save-btn": "Enregistrer",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Déconnexion",
|
||||
"sign-in-tooltip": "Connexion",
|
||||
"sign-in-welcome": "Bonjour {username} !",
|
||||
"hide": "Masquer",
|
||||
"open": "Ouvrir"
|
||||
"hide": "Masquer"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Version de Dashy",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "Couleurs personnalisées pour {theme} supprimées"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Enregistrer localement",
|
||||
"location-local-label": "Appliquer localement",
|
||||
"location-disk-label": "Appliquer dans le fichier de configuration",
|
||||
"save-button": "Enregistrer",
|
||||
"preview-button": "Prévisualiser les modifications",
|
||||
"valid-label": "La configuration est valide",
|
||||
"status-success-msg": "Tâche terminée",
|
||||
"status-fail-msg": "Échec de la tâche",
|
||||
"success-msg-disk": "Le fichier de configuration est écrit avec succès sur le disque",
|
||||
"success-msg-local": "Les modifications locales ont bien été enregistrées",
|
||||
"success-note-l1": "L'application devrait se reconstruire automatiquement.",
|
||||
"success-note-l2": "Cela peut prendre une minute.",
|
||||
"success-note-l3": "Vous devrez actualiser la page pour que les modifications prennent effet.",
|
||||
"error-msg-save-mode": "Veuillez sélectionner un mode d'enregistrement : Local ou Fichier",
|
||||
"error-msg-cannot-save": "Une erreur s'est produite lors de l'enregistrement de la configuration",
|
||||
"error-msg-bad-json": "Erreur dans le fichier JSON, il est peut-être mal formé",
|
||||
"warning-msg-validation": "Attention",
|
||||
"not-admin-note": "Vous ne pouvez pas écrire les modifications sur le disque, car vous n'êtes pas connecté en tant qu'administrateur"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "Pour plus d'informations, veuillez consulter la",
|
||||
"intro-docs": "documentation",
|
||||
"backup-title-setup": "Sauvegarde",
|
||||
"backup-title-update": "Mettre à jour la sauvegarde",
|
||||
"password-label-setup": "Choisissez un mot de passe",
|
||||
"password-label-update": "Entrer votre mot de passe",
|
||||
"backup-button-setup": "Sauvegarder",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Sauvegarde effectuée avec succès",
|
||||
"restore-success-msg": "Configuration restaurée avec succès"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Ouvrir ...",
|
||||
"sametab": "Ouvrir dans l'onglet actuel",
|
||||
"newtab": "Ouvrir dans un nouvel onglet",
|
||||
"modal": "Ouvrir en mode fenêtré",
|
||||
"workspace": "Ouvrir en plein écran",
|
||||
"options-section-title": "Options",
|
||||
"edit-item": "Modifier",
|
||||
"move-item": "Copier et Déplacer",
|
||||
"remove-item": "Supprimer"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Ouvrir ...",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Ouvrir",
|
||||
"edit-section": "Modifier",
|
||||
"expand-collapse": "Développer / Réduire",
|
||||
"move-section": "Déplacer vers",
|
||||
"remove-section": "Supprimer"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "pour éviter des conséquences inattendues."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exporter la configuration",
|
||||
"copy-clipboard-btn": "Copier dans le presse-papiers",
|
||||
"copy-clipboard-tooltip": "Copier la configuration complète de l'application sur votre appareil dans un fichier YAML",
|
||||
"download-file-btn": "Télécharger le fichier",
|
||||
@@ -285,7 +253,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Chargement ...",
|
||||
"show-more": "Développer les détails",
|
||||
"show-less": "Montrer moins",
|
||||
"open-link": "Lire la suite"
|
||||
@@ -324,13 +291,9 @@
|
||||
"good-service-rest": "Bon service sur toutes les autres lignes"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Téléchargement",
|
||||
"upload": "Envoi",
|
||||
"downloaded": "Téléchargé",
|
||||
"uploaded": "Envoyer",
|
||||
"remaining": "Restant",
|
||||
"up": "Up",
|
||||
"down": "Down"
|
||||
"remaining": "Restant"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Información da aplicación",
|
||||
"support-dashy-link": "Contribucións",
|
||||
"report-bug": "Informar dun erro",
|
||||
"report-bug-description": "Se atopaches un erro, por favor",
|
||||
"report-bug-link": "abre unha incidencia",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "Política de seguridade",
|
||||
"license": "Licenza",
|
||||
"license-under": "Licenciado baixo",
|
||||
"licence-third-party": "Para licenzas de módulos de terceiros, consulta",
|
||||
"licence-third-party-link": "Legal",
|
||||
"list-contributors": "Para a lista completa de contribuíntes e agradecementos, consulta",
|
||||
"list-contributors-link": "Créditos",
|
||||
"version": "Versión"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Deberías facer unha copia de seguridade de calquera cambio que fixeches localmente, se queres usalos no futuro.",
|
||||
"reset-config-msg-l3": "Seguro que queres continuar?",
|
||||
"data-cleared-msg": "Datos eliminados correctamente",
|
||||
"actions-label": "Accións",
|
||||
"copy-config-label": "Copiar configuración",
|
||||
"data-copied-msg": "A configuración foi copiada ao portapapeis",
|
||||
"reset-config-label": "Restablecer configuración",
|
||||
"css-save-btn": "Gardar cambios",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Pechar sesión",
|
||||
"sign-in-tooltip": "Iniciar sesión",
|
||||
"sign-in-welcome": "Ola {username}!",
|
||||
"hide": "Ocultar",
|
||||
"open": "Abrir"
|
||||
"hide": "Ocultar"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Versión de Dashy",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "Eliminadas as cores personalizadas de {theme}"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Localización de gardado",
|
||||
"location-local-label": "Aplicar localmente",
|
||||
"location-disk-label": "Gardar cambios no ficheiro de configuración",
|
||||
"save-button": "Gardar cambios",
|
||||
"preview-button": "Previsualizar cambios",
|
||||
"valid-label": "A configuración é válida",
|
||||
"status-success-msg": "Tarefa completada",
|
||||
"status-fail-msg": "Fallou a tarefa",
|
||||
"success-msg-disk": "Ficheiro de configuración gardado no disco correctamente",
|
||||
"success-msg-local": "Cambios locais gardados correctamente",
|
||||
"success-note-l1": "A aplicación debe reconstruírse automaticamente.",
|
||||
"success-note-l2": "Isto pode levar ata un minuto.",
|
||||
"success-note-l3": "Terás que actualizar a páxina para que se apliquen os cambios.",
|
||||
"error-msg-save-mode": "Selecciona un modo de gardado: Local ou Ficheiro",
|
||||
"error-msg-cannot-save": "Produciuse un erro ao gardar a configuración",
|
||||
"error-msg-bad-json": "Erro en JSON, posiblemente malformado",
|
||||
"warning-msg-validation": "Aviso de validación",
|
||||
"not-admin-note": "Non podes gardar cambios no disco porque non iniciaches sesión como administrador"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "Para obter máis información, consulta a",
|
||||
"intro-docs": "documentación",
|
||||
"backup-title-setup": "Facer unha copia de seguridade",
|
||||
"backup-title-update": "Actualizar copia de seguridade",
|
||||
"password-label-setup": "Escolle un contrasinal",
|
||||
"password-label-update": "Introduce o teu contrasinal",
|
||||
"backup-button-setup": "Copia de seguridade",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Completado con éxito",
|
||||
"restore-success-msg": "Configuración restaurada con éxito"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Abrir en",
|
||||
"sametab": "Lapela actual",
|
||||
"newtab": "Nova lapela",
|
||||
"modal": "Diálogo emerxente",
|
||||
"workspace": "Vista do espazo de traballo",
|
||||
"options-section-title": "Opcións",
|
||||
"edit-item": "Editar",
|
||||
"move-item": "Copiar ou mover",
|
||||
"remove-item": "Eliminar"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Abrir en",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Abrir sección",
|
||||
"edit-section": "Editar",
|
||||
"expand-collapse": "Expandir / Colapsar",
|
||||
"move-section": "Mover a",
|
||||
"remove-section": "Eliminar"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "para evitar consecuencias non desexadas."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exportar configuración",
|
||||
"copy-clipboard-btn": "Copiar ao portapapeis",
|
||||
"copy-clipboard-tooltip": "Copiar toda a configuración da aplicación ao portapapeis do sistema, en formato YAML",
|
||||
"download-file-btn": "Descargar como ficheiro",
|
||||
@@ -285,7 +253,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Cargando...",
|
||||
"show-more": "Expandir detalles",
|
||||
"show-less": "Amosar menos",
|
||||
"open-link": "Continuar lendo"
|
||||
@@ -324,13 +291,9 @@
|
||||
"good-service-rest": "Bos servizos en todas as outras liñas"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Descargar",
|
||||
"upload": "Subir",
|
||||
"downloaded": "Descargado",
|
||||
"uploaded": "Subido",
|
||||
"remaining": "Restante",
|
||||
"up": "Arriba",
|
||||
"down": "Abaixo"
|
||||
"remaining": "Restante"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "IP da VPN",
|
||||
|
||||
@@ -50,8 +50,6 @@
|
||||
"reset-config-msg-l2": "यदि आप भविष्य में उनका उपयोग करना चाहते हैं, तो आपको पहले स्थानीय रूप से किए गए किसी भी परिवर्तन का बैकअप लेना चाहिए।",
|
||||
"reset-config-msg-l3": "क्या आप सुनिश्चित रूप से आगे बढ़ना चाहते हैं?",
|
||||
"data-cleared-msg": "डेटा सफलतापूर्वक साफ़ किया गया",
|
||||
"actions-label": "कार्रवाई",
|
||||
"copy-config-label": "कॉन्फिग कॉपी करें",
|
||||
"data-copied-msg": "कॉन्फिग को क्लिपबोर्ड पर कॉपी कर दिया गया है",
|
||||
"reset-config-label": "कॉन्फ़िगर रीसेट करें",
|
||||
"css-save-btn": "परिवर्तनों को सुरक्षित करें",
|
||||
@@ -107,22 +105,10 @@
|
||||
"reset-toast": "{थीम} के लिए कस्टम रंग निकाले गए"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "स्थान सहेजें",
|
||||
"location-local-label": "स्थानीय रूप से आवेदन करें",
|
||||
"location-disk-label": "कॉन्फ़िग फ़ाइल में परिवर्तन लिखें",
|
||||
"save-button": "परिवर्तनों को सुरक्षित करें",
|
||||
"valid-label": "कॉन्फिग मान्य है",
|
||||
"status-success-msg": "कार्य पूर्ण",
|
||||
"status-fail-msg": "कार्य विफल",
|
||||
"success-msg-disk": "डिस्क पर सफलतापूर्वक लिखी गई कॉन्फ़िग फ़ाइल",
|
||||
"success-msg-local": "स्थानीय परिवर्तन सफलतापूर्वक सहेजे गए",
|
||||
"success-note-l1": "ऐप को स्वचालित रूप से पुनर्निर्माण करना चाहिए।",
|
||||
"success-note-l2": "इसमें एक मिनट तक का समय लग सकता है।",
|
||||
"success-note-l3": "परिवर्तनों को प्रभावी करने के लिए आपको पृष्ठ को रीफ्रेश करना होगा।",
|
||||
"error-msg-save-mode": "कृपया एक सहेजें मोड चुनें: स्थानीय या फ़ाइल",
|
||||
"error-msg-cannot-save": "कॉन्फ़िगरेशन सहेजने में त्रुटि हुई",
|
||||
"error-msg-bad-json": "JSON में त्रुटि, संभवतः विकृत",
|
||||
"warning-msg-validation": "सत्यापन चेतावनी",
|
||||
"not-admin-note": "आप डिस्क में बदला हुआ नहीं लिख सकते, क्योंकि आप एक व्यवस्थापक के रूप में लॉग इन नहीं हैं"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -131,7 +117,6 @@
|
||||
"intro-l2": "कुंजी के रूप में आपके पासवर्ड का उपयोग करते हुए, सभी डेटा एईएस के साथ पूरी तरह से एंड-टू-एंड एन्क्रिप्टेड है।",
|
||||
"intro-l3": "अधिक जानकारी के लिए, कृपया देखें",
|
||||
"backup-title-setup": "एक बैकअप बनाओ",
|
||||
"backup-title-update": "बैकअप अपडेट करें",
|
||||
"password-label-setup": "एक पासवर्ड चुनें",
|
||||
"password-label-update": "अपना कूटशब्द भरें",
|
||||
"backup-button-setup": "बैकअप",
|
||||
@@ -147,11 +132,5 @@
|
||||
"backup-error-password": "गलत पासवर्ड। कृपया अपना वर्तमान पासवर्ड दर्ज करें।",
|
||||
"backup-success-msg": "सफलतापुर्वक पूरा",
|
||||
"restore-success-msg": "कॉन्फ़िगरेशन सफलतापूर्वक पुनर्स्थापित किया गया"
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "वर्तमान टैब में खोलें",
|
||||
"newtab": "वेब टेब में खोलें",
|
||||
"modal": "पॉप-अप मोडल में खोलें",
|
||||
"workspace": "कार्यक्षेत्र दृश्य में खोलें"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "App Info",
|
||||
"support-dashy-link": "közreműködők oldalát",
|
||||
"report-bug": "Hiba bejelentése",
|
||||
"report-bug-description": "Ha úgy gondolod, hogy hibát találtál, kérlek",
|
||||
"report-bug-link": "jelentsd be itt",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "biztonsági szabályzatunk alapján",
|
||||
"license": "Licenc",
|
||||
"license-under": "Licenc típusa:",
|
||||
"licence-third-party": "A harmadik féltől származó modulok licenceiért látogasd meg a",
|
||||
"licence-third-party-link": "jogi oldalunkat",
|
||||
"list-contributors": "A teljes közreműködői lista és köszönetnyilvánítás itt található:",
|
||||
"list-contributors-link": "Készítők",
|
||||
"version": "Verzió"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Mentsd el a helyi változtatásokat, ha később is használni akarod őket",
|
||||
"reset-config-msg-l3": "Biztosan folytatod?",
|
||||
"data-cleared-msg": "Az adatok sikeresen törölve",
|
||||
"actions-label": "Műveletek",
|
||||
"copy-config-label": "Beállítások másolása",
|
||||
"data-copied-msg": "A konfiguráció a vágólapra másolva",
|
||||
"reset-config-label": "Beállítások visszaállítása",
|
||||
"css-save-btn": "Változtatások mentése",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Kijelentkezés",
|
||||
"sign-in-tooltip": "Bejelentkezés",
|
||||
"sign-in-welcome": "Szia, {username}!",
|
||||
"hide": "Elrejtés",
|
||||
"open": "Megnyitás"
|
||||
"hide": "Elrejtés"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy verzió",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "A(z) {theme} egyéni színei törölve"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Mentés helye",
|
||||
"location-local-label": "Helyi mentés",
|
||||
"location-disk-label": "Változások mentése a konfigurációs fájlba",
|
||||
"save-button": "Változtatások mentése",
|
||||
"preview-button": "Változtatások előnézete",
|
||||
"valid-label": "A konfiguráció érvényes",
|
||||
"status-success-msg": "Feladat sikeresen befejezve",
|
||||
"status-fail-msg": "A feladat sikertelen volt",
|
||||
"success-msg-disk": "A konfigurációs fájl sikeresen mentve a lemezre",
|
||||
"success-msg-local": "A helyi változtatások sikeresen elmentve",
|
||||
"success-note-l1": "A változtatások érvényesítéséhez frissítened kell az oldalt.",
|
||||
"success-note-l2": "",
|
||||
"success-note-l3": "",
|
||||
"error-msg-save-mode": "Válaszd ki a mentés módját: helyi vagy fájlba mentés",
|
||||
"error-msg-cannot-save": "Hiba történt a konfiguráció mentése közben",
|
||||
"error-msg-bad-json": "Hibás JSON-formátum, valószínűleg rosszul van megadva",
|
||||
"warning-msg-validation": "Érvényesítési figyelmeztetés",
|
||||
"not-admin-note": "Nem írhatsz a lemezre, mert nem rendszergazdaként vagy bejelentkezve"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "További információért lásd a",
|
||||
"intro-docs": "dokumentációt",
|
||||
"backup-title-setup": "Biztonsági mentés készítése",
|
||||
"backup-title-update": "Mentés frissítése",
|
||||
"password-label-setup": "Válassz jelszót",
|
||||
"password-label-update": "Add meg a jelszavad",
|
||||
"backup-button-setup": "Biztonsági mentés",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Sikeresen befejezve",
|
||||
"restore-success-msg": "A beállítások sikeresen visszaállítva"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Megnyitás itt",
|
||||
"sametab": "Jelenlegi fülön",
|
||||
"newtab": "Új fülön",
|
||||
"modal": "Felugró ablakban",
|
||||
"workspace": "Munkaterület nézet",
|
||||
"options-section-title": "Beállítások",
|
||||
"edit-item": "Szerkesztés",
|
||||
"move-item": "Másolás vagy áthelyezés",
|
||||
"remove-item": "Eltávolítás"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Megnyitás itt",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Szakasz megnyitása",
|
||||
"edit-section": "Szerkesztés",
|
||||
"expand-collapse": "Kinyitás / Összecsukás",
|
||||
"move-section": "Áthelyezés ide",
|
||||
"remove-section": "Eltávolítás"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "hogy elkerüld a nem kívánt következményeket."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Konfiguráció exportálása",
|
||||
"copy-clipboard-btn": "Másolás vágólapra",
|
||||
"copy-clipboard-tooltip": "Az összes beállítás másolása a vágólapra YAML formátumban",
|
||||
"download-file-btn": "Letöltés fájlként",
|
||||
@@ -293,7 +261,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Betöltés",
|
||||
"show-more": "Részletek megjelenítése",
|
||||
"cpu-details": "CPU adatai",
|
||||
"mem-details": "Mamória adatai",
|
||||
@@ -334,13 +301,9 @@
|
||||
"good-service-rest": "Minden más vonalon zavartalan a közlekedés"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Letöltés",
|
||||
"upload": "Feltöltés",
|
||||
"downloaded": "Letöltve",
|
||||
"uploaded": "Feltöltve",
|
||||
"remaining": "Hátralévő",
|
||||
"up": "Feltöltés",
|
||||
"down": "Letöltés"
|
||||
"remaining": "Hátralévő"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP-cím",
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
"reset-config-msg-l2": "Dovresti prima eseguire il backup di tutte le modifiche apportate localmente, se desideri utilizzarle in futuro.",
|
||||
"reset-config-msg-l3": "Sei sicuro di voler procedere?",
|
||||
"data-cleared-msg": "Dati cancellati con successo",
|
||||
"actions-label": "Azioni",
|
||||
"copy-config-label": "Copia configurazione",
|
||||
"data-copied-msg": "La configurazione è stata copiata negli appunti",
|
||||
"reset-config-label": "Ripristina configurazione",
|
||||
"css-save-btn": "Salvare le modifiche",
|
||||
@@ -110,23 +108,11 @@
|
||||
"reset-toast": "Rimossi Colori personalizzati per {theme}"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Posizione del salvataggio",
|
||||
"location-local-label": "Salvataggio Locale",
|
||||
"location-disk-label": "Salva le modifiche nel file di configurazione",
|
||||
"save-button": "Salvare le modifiche",
|
||||
"preview-button": "Preview Changes",
|
||||
"valid-label": "La configurazione è valida",
|
||||
"status-success-msg": "Attività completata",
|
||||
"status-fail-msg": "Attività fallita",
|
||||
"success-msg-disk": "File di configurazione scritto correttamente su disco",
|
||||
"success-msg-local": "Modifiche locali salvate correttamente",
|
||||
"success-note-l1": "L'app dovrebbe ricompilarsi automaticamente.",
|
||||
"success-note-l2": "Questa operazione potrebbe richiedere fino a un minuto.",
|
||||
"success-note-l3": "Sarà necessario aggiornare la pagina affinché le modifiche abbiano effetto.",
|
||||
"error-msg-save-mode": "Seleziona una modalità di salvataggio: Localmente o in un File",
|
||||
"error-msg-cannot-save": "Si è verificato un errore durante il salvataggio della configurazione",
|
||||
"error-msg-bad-json": "Errore nella struttura JSON, probabilmente non formattata correttamente",
|
||||
"warning-msg-validation": "Avviso di convalida",
|
||||
"not-admin-note": "Non puoi scrivere le modifiche su disco, perché non sei autenticato come amministratore"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -135,7 +121,6 @@
|
||||
"intro-l2": "Tutti i dati sono completamente crittografati end-to-end con AES, utilizzando la tua password come chiave.",
|
||||
"intro-l3": "Per maggiori informazioni, vedere il",
|
||||
"backup-title-setup": "Fai un backup",
|
||||
"backup-title-update": "Aggiorna backup",
|
||||
"password-label-setup": "Scegli una password",
|
||||
"password-label-update": "Inserisci la tua password",
|
||||
"backup-button-setup": "Backup",
|
||||
@@ -152,17 +137,6 @@
|
||||
"backup-success-msg": "Completato con successo",
|
||||
"restore-success-msg": "Configurazione ripristinata con successo"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Open In",
|
||||
"sametab": "Tab Attuale",
|
||||
"newtab": "Nuovo Tab",
|
||||
"modal": "Pop-Up",
|
||||
"workspace": "Workspace",
|
||||
"options-section-title": "Opzioni",
|
||||
"edit-item": "Edita",
|
||||
"move-item": "Copia/Sposta",
|
||||
"remove-item": "Rimuovi"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Apri In",
|
||||
@@ -180,7 +154,6 @@
|
||||
"section": {
|
||||
"open-section": "Apri Sezione",
|
||||
"edit-section": "Edita",
|
||||
"move-section": "Sposta in",
|
||||
"remove-section": "Rimuovi"
|
||||
}
|
||||
},
|
||||
@@ -228,7 +201,6 @@
|
||||
"warning-msg-l3": "per evitare spiacevoli conseguenze."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Esporta Configurazione",
|
||||
"copy-clipboard-btn": "Copia negli Appunti",
|
||||
"copy-clipboard-tooltip": "Copia la configurazione negli appunti, in formato YAML",
|
||||
"download-file-btn": "Salva file",
|
||||
@@ -237,7 +209,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Caricamento...",
|
||||
"show-more": "Dettagli",
|
||||
"show-less": "Minori dettagli",
|
||||
"open-link": "Continua Lettura"
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "アプリ情報",
|
||||
"support-dashy-link": "コントリビューションページ",
|
||||
"report-bug": "バグの報告",
|
||||
"report-bug-description": "バグを発見したと思われる場合は、Issueを立ててください: ",
|
||||
"report-bug-link": "Issueを立てる",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "セキュリティーポリシー",
|
||||
"license": "ライセンス",
|
||||
"license-under": "Licensed under",
|
||||
"licence-third-party": "サードパーティーモジュールのライセンスは、こちらをご覧ください: ",
|
||||
"licence-third-party-link": "リーガル",
|
||||
"list-contributors": "すべてのコントリビューターのリストは、こちらをご覧ください: ",
|
||||
"list-contributors-link": "クレジット",
|
||||
"version": "バージョン"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "後でローカルで行った変更を使用したい場合は、先にバックアップしてください。",
|
||||
"reset-config-msg-l3": "続行しますか?",
|
||||
"data-cleared-msg": "データは正常にクリアされました",
|
||||
"actions-label": "アクション",
|
||||
"copy-config-label": "設定のコピー",
|
||||
"data-copied-msg": "設定がクリップボードにコピーされました",
|
||||
"reset-config-label": "設定のリセット",
|
||||
"css-save-btn": "変更内容を保存",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "サインアウト",
|
||||
"sign-in-tooltip": "ログイン",
|
||||
"sign-in-welcome": "{username}さん、こんにちは!",
|
||||
"hide": "閉じる",
|
||||
"open": "開く"
|
||||
"hide": "閉じる"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashyのバージョン",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "{theme}のカスタムカラーが削除されました"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "保存する場所",
|
||||
"location-local-label": "ローカルで適用",
|
||||
"location-disk-label": "設定ファイルに変更を書き込む",
|
||||
"save-button": "変更内容を保存",
|
||||
"preview-button": "変更内容をプレビュー",
|
||||
"valid-label": "設定は有効です",
|
||||
"status-success-msg": "タスクが完了しました",
|
||||
"status-fail-msg": "タスクが失敗しました",
|
||||
"success-msg-disk": "設定ファイルがディスクに正常に書き込まれました",
|
||||
"success-msg-local": "ローカルの変更が正常に保存されました",
|
||||
"success-note-l1": "アプリは自動的に再構築されます。",
|
||||
"success-note-l2": "これには最大1分かかる場合があります。",
|
||||
"success-note-l3": "変更を適用にするには、ページを更新する必要があります。",
|
||||
"error-msg-save-mode": "保存モードを選択してください (ローカルまたはファイル)",
|
||||
"error-msg-cannot-save": "設定の保存中にエラーが発生しました",
|
||||
"error-msg-bad-json": "JSONに間違いがあります。形式が間違っているかもしれません",
|
||||
"warning-msg-validation": "バリデーション警告",
|
||||
"not-admin-note": "管理者としてログインしていないため、変更をディスクに書き込むことはできません"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "詳細については、以下をご覧ください: ",
|
||||
"intro-docs": "ドキュメント",
|
||||
"backup-title-setup": "バックアップの作成",
|
||||
"backup-title-update": "バックアップの更新",
|
||||
"password-label-setup": "パスワード",
|
||||
"password-label-update": "パスワードを入力してください",
|
||||
"backup-button-setup": "バックアップ",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "正常に完了しました",
|
||||
"restore-success-msg": "設定が正常に復元されました"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "開く",
|
||||
"sametab": "現在のタブで開く",
|
||||
"newtab": "新しいタブで開く",
|
||||
"modal": "ポップアップモーダルで開く",
|
||||
"workspace": "ワークスペース表示で開く",
|
||||
"options-section-title": "編集",
|
||||
"edit-item": "編集",
|
||||
"move-item": "コピー・移動",
|
||||
"remove-item": "削除"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "開く",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "セクションを開く",
|
||||
"edit-section": "編集",
|
||||
"expand-collapse": "開く / 閉じる",
|
||||
"move-section": "移動",
|
||||
"remove-section": "削除"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "を参照し、意図しない結果にならないように注意してください。"
|
||||
},
|
||||
"export": {
|
||||
"export-title": "設定をエクスポート",
|
||||
"copy-clipboard-btn": "クリップボードにコピー",
|
||||
"copy-clipboard-tooltip": "すべてのアプリ設定をシステムのクリップボードにYAML形式でコピーします",
|
||||
"download-file-btn": "ファイルとしてダウンロード",
|
||||
@@ -285,7 +253,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "読み込み中...",
|
||||
"show-more": "詳細を開く",
|
||||
"show-less": "詳細を閉じる",
|
||||
"open-link": "続きを読む"
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
"reset-config-msg-l2": "나중에 사용하려면 먼저 로컬에서 변경한 내용을 백업해야 합니다.",
|
||||
"reset-config-msg-l3": "계속하시겠습니까?",
|
||||
"data-cleared-msg": "데이터 삭제 성공",
|
||||
"actions-label": "동작",
|
||||
"copy-config-label": "설정 복사",
|
||||
"data-copied-msg": "설정이 클립보드에 복사되었습니다.",
|
||||
"reset-config-label": "설정 초기화",
|
||||
"css-save-btn": "변경 사항을 저장",
|
||||
@@ -110,23 +108,11 @@
|
||||
"reset-toast": "C{theme}에 대한 사용자 정의 색상이 제거되었습니다."
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "저장 위치",
|
||||
"location-local-label": "로컬로 적용",
|
||||
"location-disk-label": "설정 파일에 변경 사항 쓰기",
|
||||
"save-button": "변경 사항 저장",
|
||||
"preview-button": "변경 사항 미리보기",
|
||||
"valid-label": "유효한 설정",
|
||||
"status-success-msg": "작업 완료",
|
||||
"status-fail-msg": "작업 실패",
|
||||
"success-msg-disk": "설정 파일이 디스크에 성공적으로 저장되었습니다.",
|
||||
"success-msg-local": "로컬 변경 사항이 성공적으로 저장되었습니다.",
|
||||
"success-note-l1": "앱이 자동으로 다시 빌드됩니다.",
|
||||
"success-note-l2": "이 작업은 최대 몇 분이 걸릴 수 있습니다.",
|
||||
"success-note-l3": "변경 사항을 적용하려면 페이지를 새로고침해야 합니다.",
|
||||
"error-msg-save-mode": "저장 모드를 선택하세요: 로컬 또는 파일",
|
||||
"error-msg-cannot-save": "설정을 저장하는 동안 오류가 발생했습니다.",
|
||||
"error-msg-bad-json": "JSON 오류, 형식이 잘못되었을 수 있습니다.",
|
||||
"warning-msg-validation": "유효성 검증 경고",
|
||||
"not-admin-note": "관리자로 로그인하지 않았기 때문에 디스크에 변경 사항을 저장할 수 없습니다."
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -135,7 +121,6 @@
|
||||
"intro-l2": "모든 데이터는 암호를 키로 사용하여 AES로 종단 간 암호화됩니다..",
|
||||
"intro-l3": "자세한 내용은",
|
||||
"backup-title-setup": "백업 생성하기",
|
||||
"backup-title-update": "백업 업데이트",
|
||||
"password-label-setup": "비밀번호 선택",
|
||||
"password-label-update": "비밀번호를 입력하세요",
|
||||
"backup-button-setup": "백업",
|
||||
@@ -152,17 +137,6 @@
|
||||
"backup-success-msg": "성공적으로 완료됨",
|
||||
"restore-success-msg": "설정이 성공적으로 복원되었습니다."
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "열기",
|
||||
"sametab": "현재 탭",
|
||||
"newtab": "새로운 탭",
|
||||
"modal": "팝업 모달",
|
||||
"workspace": "작업 공간 보기",
|
||||
"options-section-title": "옵션",
|
||||
"edit-item": "수정",
|
||||
"move-item": "복사 또는 이동",
|
||||
"remove-item": "제거"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "열기",
|
||||
@@ -181,7 +155,6 @@
|
||||
"open-section": "섹션 열기",
|
||||
"edit-section": "수정",
|
||||
"expand-collapse": "펼치기 / 접기",
|
||||
"move-section": "이동",
|
||||
"remove-section": "제거"
|
||||
}
|
||||
},
|
||||
@@ -232,7 +205,6 @@
|
||||
"warning-msg-l3": "의도하지 않은 결과를 피하기 위해."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "설정 내보내기",
|
||||
"copy-clipboard-btn": "클립보드에 복사",
|
||||
"copy-clipboard-tooltip": "모든 앱 설정을 YAML 형식으로 시스템 클립보드에 복사",
|
||||
"download-file-btn": "파일로 다운로드",
|
||||
@@ -241,7 +213,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "로딩중...",
|
||||
"show-more": "더보기",
|
||||
"show-less": "줄이기",
|
||||
"open-link": "계속 읽기"
|
||||
@@ -280,13 +251,9 @@
|
||||
"good-service-rest": "다른 모든 노선 원활히 운행 중"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "다운로드",
|
||||
"upload": "업로드",
|
||||
"downloaded": "다운로드됨",
|
||||
"uploaded": "업로드됨",
|
||||
"remaining": "남은 작업",
|
||||
"up": "위로",
|
||||
"down": "아래로"
|
||||
"remaining": "남은 작업"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Тиркеме жөнүндө маалымат",
|
||||
"support-dashy-link": "Салым кошкондор бети",
|
||||
"report-bug": "Ката тууралуу билдирүү",
|
||||
"report-bug-description": "Эгер ката таптым деп ойлосоңуз",
|
||||
"report-bug-link": "маселе ачуу",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "Коопсуздук саясаты",
|
||||
"license": "Лицензия",
|
||||
"license-under": "Лицензиясы",
|
||||
"licence-third-party": "Үчүнчү тарап модулдарынын лицензиялары жөнүндө",
|
||||
"licence-third-party-link": "Юридикалык маалымат",
|
||||
"list-contributors": "Салым кошкондордун толук тизмеси жана ыраазычылык билдирүү үчүн",
|
||||
"list-contributors-link": "Кредиттер",
|
||||
"version": "Версия"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Жергиликтүү өзгөртүүлөрдү келечекте колдонуу үчүн алдын ала камдык көчүрмөсүн сактаңыз.",
|
||||
"reset-config-msg-l3": "Улантууну каалайсызбы?",
|
||||
"data-cleared-msg": "Дайындар ийгиликтүү тазаланды",
|
||||
"actions-label": "Аракеттер",
|
||||
"copy-config-label": "Конфигурацияны көчүрүү",
|
||||
"data-copied-msg": "Конфигурация алмашуу буферине көчүрүлдү",
|
||||
"reset-config-label": "Конфигурацияны баштапкы абалга келтирүү",
|
||||
"css-save-btn": "Өзгөртүүлөрдү сактоо",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Чыгуу",
|
||||
"sign-in-tooltip": "Кирүү",
|
||||
"sign-in-welcome": "Салам, {username}!",
|
||||
"hide": "Жашыруу",
|
||||
"open": "Ачуу"
|
||||
"hide": "Жашыруу"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy версиясы",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "{theme} темасынын өзгөчө түстөрү өчүрүлдү"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Сактоо жайгашуусу",
|
||||
"location-local-label": "Жергиликтүү (браузерде)",
|
||||
"location-disk-label": "Дисктеги конфигурация файлына жазуу",
|
||||
"save-button": "Өзгөртүүлөрдү сактоо",
|
||||
"preview-button": "Өзгөртүүлөрдү алдын ала көрүү",
|
||||
"valid-label": "Конфигурация туура",
|
||||
"status-success-msg": "Тапшырма ийгиликтүү аткарылды",
|
||||
"status-fail-msg": "Тапшырма аткарылган жок",
|
||||
"success-msg-disk": "Конфигурация файлы дискке ийгиликтүү жазылды",
|
||||
"success-msg-local": "Жергиликтүү өзгөртүүлөр сакталды",
|
||||
"success-note-l1": "Өзгөртүүлөр күчүнө кириши үчүн баракты жаңыртыңыз.",
|
||||
"success-note-l2": "",
|
||||
"success-note-l3": "",
|
||||
"error-msg-save-mode": "Сактоо режимин тандаңыз: Жергиликтүү же Диск",
|
||||
"error-msg-cannot-save": "Конфигурацияны сактоодо ката кетти",
|
||||
"error-msg-bad-json": "JSON'до ката, форматы туура эмес болушу мүмкүн",
|
||||
"warning-msg-validation": "Валидация эскертүүсү",
|
||||
"not-admin-note": "Дискке жазуу укугу жок, администратор катары кирүү керек"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "Көбүрөөк маалымат алуу үчүн",
|
||||
"intro-docs": "документацияны караңыз",
|
||||
"backup-title-setup": "Камдык көчүрмө түзүү",
|
||||
"backup-title-update": "Камдык көчүрмөнү жаңыртуу",
|
||||
"password-label-setup": "Сырсөз түзүңүз",
|
||||
"password-label-update": "Учурдагы сырсөзүңүздү киргизиңиз",
|
||||
"backup-button-setup": "Камдык көчүрмө түзүү",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Ийгиликтүү аяктады",
|
||||
"restore-success-msg": "Конфигурация ийгиликтүү калыбына келтирилди"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Ачуу жолу",
|
||||
"sametab": "Учурдагы таб",
|
||||
"newtab": "Жаңы таб",
|
||||
"modal": "Ачылуучу терезе",
|
||||
"workspace": "Жумуш мейкиндиги көрүнүшү",
|
||||
"options-section-title": "Жөндөөлөр",
|
||||
"edit-item": "Түзөтүү",
|
||||
"move-item": "Көчүрүү же жылдыруу",
|
||||
"remove-item": "Өчүрүү"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Ачуу жолу",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Бөлүмдү ачуу",
|
||||
"edit-section": "Түзөтүү",
|
||||
"expand-collapse": "Жайуу / Жыйноо",
|
||||
"move-section": "Жылдыруу",
|
||||
"remove-section": "Өчүрүү"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "керексиз кесепеттерден качуу үчүн."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Конфигурацияны экспорттоо",
|
||||
"copy-clipboard-btn": "Алмашуу буферине көчүрүү",
|
||||
"copy-clipboard-tooltip": "Конфигурацияны YAML форматында алмашуу буферине көчүрүү",
|
||||
"download-file-btn": "Файл катары жүктөө",
|
||||
@@ -293,7 +261,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Жүктөлүүдө...",
|
||||
"show-more": "Көбүрөөк чоо-жай",
|
||||
"cpu-details": "Процессор чоо-жайы",
|
||||
"mem-details": "Эстутум чоо-жайы",
|
||||
@@ -334,13 +301,9 @@
|
||||
"good-service-rest": "Калган линияларда кызмат жакшы"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Жүктөө",
|
||||
"upload": "Жүктөө (upload)",
|
||||
"downloaded": "Жүктөлгөн",
|
||||
"uploaded": "Жүктөлгөн (upload)",
|
||||
"remaining": "Калганы",
|
||||
"up": "Өйдө",
|
||||
"down": "Төмөн"
|
||||
"remaining": "Калганы"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP дареги",
|
||||
|
||||
@@ -27,9 +27,7 @@
|
||||
"logout-message": "Logget ut",
|
||||
"already-logged-in-title": "Allerede logget inn",
|
||||
"already-logged-in-text": "Du er logget inn som",
|
||||
"continue-to-dashboard": "Fortsett til dashbordet",
|
||||
"log-out-button": "Logg ut",
|
||||
"continue-guest-button": "Fortsett som gjest"
|
||||
"log-out-button": "Logg ut"
|
||||
},
|
||||
"config": {
|
||||
"main-tab": "Hovedmeny",
|
||||
@@ -50,8 +48,6 @@
|
||||
"reset-config-msg-l2": "Du bør først ta sikkerhetskopi av eventuelle endringer du har gjort lokalt, hvis du vil bruke dem i fremtiden.",
|
||||
"reset-config-msg-l3": "Er du sikker på at du vil fortsette?",
|
||||
"data-cleared-msg": "Data slettet vellykket",
|
||||
"actions-label": "Handlinger",
|
||||
"copy-config-label": "Kopier konfigurasjon",
|
||||
"data-copied-msg": "Konfig er kopiert til utklippstavlen",
|
||||
"reset-config-label": "Tilbakestill konfigurasjon",
|
||||
"css-save-btn": "Lagre endringer",
|
||||
@@ -107,22 +103,10 @@
|
||||
"reset-toast": "Egendefinerte farger for {theme} fjernet"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Lagre beliggenhet",
|
||||
"location-local-label": "Søk lokalt",
|
||||
"location-disk-label": "Skriv endringer i konfigurasjonsfil",
|
||||
"save-button": "Lagre endringer",
|
||||
"valid-label": "Konfigurasjon er gyldig",
|
||||
"status-success-msg": "Oppgaven fullført",
|
||||
"status-fail-msg": "Oppgaven mislyktes",
|
||||
"success-msg-disk": "Konfigurasjonsfil skrevet til disk vellykket",
|
||||
"success-msg-local": "Lokale endringer er lagret",
|
||||
"success-note-l1": "Appen bør bygge om automatisk.",
|
||||
"success-note-l2": "Dette kan ta opptil et minutt.",
|
||||
"success-note-l3": "Du må oppdatere siden for at endringene skal tre i kraft.",
|
||||
"error-msg-save-mode": "Velg en lagringsmodus: lokal eller fil",
|
||||
"error-msg-cannot-save": "Det oppsto en feil under konfigurering",
|
||||
"error-msg-bad-json": "Feil i JSON, muligens feilformet",
|
||||
"warning-msg-validation": "Valideringsadvarsel",
|
||||
"not-admin-note": "Du kan ikke skrive endret til disk, fordi du ikke er logget inn som admin"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -131,7 +115,6 @@
|
||||
"intro-l2": "Alle data er helt ende-til-ende-kryptert med AES, og bruker passordet ditt som nøkkelen.",
|
||||
"intro-l3": "For mer informasjon, se",
|
||||
"backup-title-setup": "Lag en sikkerhetskopi",
|
||||
"backup-title-update": "Oppdater sikkerhetskopi",
|
||||
"password-label-setup": "Velg et passord",
|
||||
"password-label-update": "Skriv inn passordet ditt",
|
||||
"backup-button-setup": "Sikkerhetskopiering",
|
||||
@@ -147,11 +130,5 @@
|
||||
"backup-error-password": "Feil passord. Skriv inn ditt nåværende passord.",
|
||||
"backup-success-msg": "Fullført vellykket",
|
||||
"restore-success-msg": "Konfigurasjon gjenopprettet vellykket"
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Åpne i nåværende fane",
|
||||
"newtab": "Åpne i ny fane",
|
||||
"modal": "Åpne i popup-modus",
|
||||
"workspace": "Åpne i Workspace-visning"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
"reset-config-msg-l2": "Maak eerst een backup van de lokale veranderingen, om ze later nog te kunnen gebruiken.",
|
||||
"reset-config-msg-l3": "Weet je het zeker?",
|
||||
"data-cleared-msg": "Data succesvol verwijderd",
|
||||
"actions-label": "Acties",
|
||||
"copy-config-label": "Kopieer Config",
|
||||
"data-copied-msg": "Config is gekopieerd naar het klembord",
|
||||
"reset-config-label": "Reset Config",
|
||||
"css-save-btn": "Sla Wijzigingen Op",
|
||||
@@ -85,22 +83,10 @@
|
||||
"reset-toast": "Aangepaste Kleuren voor {theme} Verwijderd"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Sla Locatie Op",
|
||||
"location-local-label": "Pas Lokaal Toe",
|
||||
"location-disk-label": "Sla Veranderingen Op in Config Bestand",
|
||||
"save-button": "Opslaan",
|
||||
"valid-label": "Config is Geldig",
|
||||
"status-success-msg": "Taak Voltooid",
|
||||
"status-fail-msg": "Taak Gefaald",
|
||||
"success-msg-disk": "Config bestand succesvol opgeslagen",
|
||||
"success-msg-local": "Lokale aanpassingen succesvol opgeslagen",
|
||||
"success-note-l1": "De applicatie zou automatisch moeten herbouwen.",
|
||||
"success-note-l2": "Dit duurt maximaal een minuut.",
|
||||
"success-note-l3": "Herlaad de pagina om de veranderingen toe te passen.",
|
||||
"error-msg-save-mode": "Selecteer een Save Mode: Lokaal of Bestand",
|
||||
"error-msg-cannot-save": "Een fout trad op tijdens het opslaan van de config",
|
||||
"error-msg-bad-json": "Fout in JSON, mogelijk ongeldige structuur",
|
||||
"warning-msg-validation": "Validatie Waarschuwing"
|
||||
"error-msg-cannot-save": "Een fout trad op tijdens het opslaan van de config"
|
||||
},
|
||||
"cloud-sync": {
|
||||
"title": "Cloud Backup & Herstel",
|
||||
@@ -108,7 +94,6 @@
|
||||
"intro-l2": "Alle data is volledig end-to-end versleuteld met AES, met je wachtwoord als de sleutel.",
|
||||
"intro-l3": "Voor meer informatie, zie",
|
||||
"backup-title-setup": "Maak een Backup",
|
||||
"backup-title-update": "Update Backup",
|
||||
"password-label-setup": "Kies een Wachtwoord",
|
||||
"password-label-update": "Voer Wachtwoord in",
|
||||
"backup-button-setup": "Backup",
|
||||
|
||||
@@ -50,8 +50,6 @@
|
||||
"reset-config-msg-l2": "Zrób kopię zapasową jeśli obecne ustawienia są ważne.",
|
||||
"reset-config-msg-l3": "Czy na pewno chcesz kontynuować?",
|
||||
"data-cleared-msg": "Dane wyczyszczone pomyślnie",
|
||||
"actions-label": "Akcje",
|
||||
"copy-config-label": "Kopia konfiguracji",
|
||||
"data-copied-msg": "Konfiguracja skopiowana do schowka",
|
||||
"reset-config-label": "Zresetuj konfigurację",
|
||||
"css-save-btn": "Zapisz zmiany",
|
||||
@@ -107,22 +105,10 @@
|
||||
"reset-toast": "Niestandardowe kolory dla {theme} usunięte"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Lokalizacja zapisu",
|
||||
"location-local-label": "Pamięć podręczna",
|
||||
"location-disk-label": "Plik na dysku",
|
||||
"save-button": "Zapisz",
|
||||
"valid-label": "Konfiguracja poprawna",
|
||||
"status-success-msg": "Zadanie ukończone",
|
||||
"status-fail-msg": "Zadanie nie powiodło się",
|
||||
"success-msg-disk": "Pomyślnie zapisano na dysku",
|
||||
"success-msg-local": "Pomyślnie zapisano w pamięci podręcznej",
|
||||
"success-note-l1": "Aplikacja powinna automatycznie się przebudować.",
|
||||
"success-note-l2": "Może to zająć około minuty.",
|
||||
"success-note-l3": "Będzie konieczne odświeżenie strony",
|
||||
"error-msg-save-mode": "Proszę wybrać pomiędzy pamięcią podręczną lub plikiem na dysku",
|
||||
"error-msg-cannot-save": "Wystąpił błąd podczas zapisywania",
|
||||
"error-msg-bad-json": "Błąd w JSON",
|
||||
"warning-msg-validation": "Ostrzeżenie",
|
||||
"not-admin-note": "Nie możesz zapisywać na dysku, wymagane uprawnienia administratora"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -131,7 +117,6 @@
|
||||
"intro-l2": "Wszystkie dane są w pełni zaszyfrowane z wykorzystaniem AES, kluczem będzie podane hasło.",
|
||||
"intro-l3": "Aby uzyskać więcej informacji przejdź do",
|
||||
"backup-title-setup": "Tworzenie",
|
||||
"backup-title-update": "Zaktualizuj",
|
||||
"password-label-setup": "Wybierz hasło",
|
||||
"password-label-update": "Wprowadź hasło",
|
||||
"backup-button-setup": "Zapisz",
|
||||
@@ -147,11 +132,5 @@
|
||||
"backup-error-password": "Hasło niepoprawne. Proszę wprowadzić aktualne hasło.",
|
||||
"backup-success-msg": "zakończono pomyślnie",
|
||||
"restore-success-msg": "Przywrócono pomyślnie"
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Otwórz w tej karcie",
|
||||
"newtab": "Otwórz w nowej karcie",
|
||||
"modal": "Otwórz w oknie modalnym",
|
||||
"workspace": "Otwórz w obszarze roboczym"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,6 @@
|
||||
"reset-config-msg-l2": "Você deve primeiro fazer um backup de todas as alterações locais, se quiser usá-las no futuro.",
|
||||
"reset-config-msg-l3": "Tem certeza que deseja continuar?",
|
||||
"data-cleared-msg": "Dados apagados",
|
||||
"actions-label": "Ações",
|
||||
"copy-config-label": "Copiar configuração",
|
||||
"data-copied-msg": "A configuração foi copiada para a área de trabalho",
|
||||
"reset-config-label": "Reset de configuração",
|
||||
"css-save-btn": "Salvar mudanças",
|
||||
@@ -124,23 +122,11 @@
|
||||
"reset-toast": "Cores customizadas do {theme} removidas"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Salvar localiação",
|
||||
"location-local-label": "Aplicar localmente",
|
||||
"location-disk-label": "Salvar alterações no arquivo de configuração",
|
||||
"save-button": "Salvar alterações",
|
||||
"preview-button": "Previsualizar",
|
||||
"valid-label": "Configuração é válida",
|
||||
"status-success-msg": "Tarefa concluída",
|
||||
"status-fail-msg": "Falha na tarefa",
|
||||
"success-msg-disk": "Arquivo de configuração gravado no disco com sucesso",
|
||||
"success-msg-local": "Mudanças locais salvas com sucesso",
|
||||
"success-note-l1": "O app deve atualizar automaticamente.",
|
||||
"success-note-l2": "Isso pode levar até um minuto.",
|
||||
"success-note-l3": "Você precisará atualizar a página para aplicar as mudanças.",
|
||||
"error-msg-save-mode": "Modo de salvar: Localmente ou Arquivo",
|
||||
"error-msg-cannot-save": "Ocorreu um erro ao salvar o arquivo",
|
||||
"error-msg-bad-json": "Erro no JSON, possivelmente mal formatado",
|
||||
"warning-msg-validation": "Erro de validação",
|
||||
"not-admin-note": "Você não pode salvar essa mudança no disco. Você não está conectado como admin"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -149,7 +135,6 @@
|
||||
"intro-l2": "Todos os dados são totalmente criptografados de ponta a ponta com AES, usando sua senha como chave.",
|
||||
"intro-l3": "Para obter mais informações, consulte o",
|
||||
"backup-title-setup": "Faça um backup",
|
||||
"backup-title-update": "Atualizar backup",
|
||||
"password-label-setup": "Escolha uma senha",
|
||||
"password-label-update": "Coloque sua senha",
|
||||
"backup-button-setup": "Backup",
|
||||
@@ -166,17 +151,6 @@
|
||||
"backup-success-msg": "Completado com sucesso",
|
||||
"restore-success-msg": "Configuração restaurada com sucesso"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Abrir em",
|
||||
"sametab": "Aba atual",
|
||||
"newtab": "Nova aba",
|
||||
"modal": "Pop-up",
|
||||
"workspace": "Visualização Workspace",
|
||||
"options-section-title": "Opções",
|
||||
"edit-item": "Editar",
|
||||
"move-item": "Copiar ou Mover",
|
||||
"remove-item": "Remover"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Abrir em",
|
||||
@@ -194,7 +168,6 @@
|
||||
"section": {
|
||||
"open-section": "Abrir seção",
|
||||
"edit-section": "Editar",
|
||||
"move-section": "Mover para",
|
||||
"remove-section": "Remover"
|
||||
}
|
||||
},
|
||||
@@ -242,7 +215,6 @@
|
||||
"warning-msg-l3": "para evitar consequências indesejadas"
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Expotar configuração",
|
||||
"copy-clipboard-btn": "Copiar para área de transferência",
|
||||
"copy-clipboard-tooltip": "Copiar toda as configurações do app para a área de transferência, no formato YAML",
|
||||
"download-file-btn": "Baixar como arquivo",
|
||||
@@ -251,7 +223,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Carregando...",
|
||||
"show-more": "Mostrar mais",
|
||||
"show-less": "Mostrar menos",
|
||||
"open-link": "Continuar leitura"
|
||||
@@ -290,13 +261,9 @@
|
||||
"good-service-rest": "Bom serviço em todas as outras linhas."
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Baixar",
|
||||
"upload": "Carregar",
|
||||
"downloaded": "Baixado",
|
||||
"uploaded": "Carregado",
|
||||
"remaining": "Restante",
|
||||
"up": "Up",
|
||||
"down": "Down"
|
||||
"remaining": "Restante"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "IP da VPN",
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Informații Aplicație",
|
||||
"support-dashy-link": "Pagina de Contribuții",
|
||||
"report-bug": "Raportează o Eroare",
|
||||
"report-bug-description": "Dacă crezi că ai găsit o eroare, atunci te rog",
|
||||
"report-bug-link": "deschide o Problemă",
|
||||
@@ -63,8 +62,6 @@
|
||||
"privacy-and-security-security-policy": "Politica de Securitate",
|
||||
"license": "Licență",
|
||||
"license-under": "Licențiat sub",
|
||||
"licence-third-party": "Pentru licențele modulelor terțe părți, vă rugăm să consultați",
|
||||
"licence-third-party-link": "Legal",
|
||||
"list-contributors": "Pentru lista completă a contribuitorilor și mulțumiri, vedeți",
|
||||
"list-contributors-link": "Credite",
|
||||
"version": "Versiune",
|
||||
@@ -96,8 +93,6 @@
|
||||
"reset-config-msg-l2": "Ar trebui să faceți mai întâi o copie de siguranță a oricăror modificări pe care le-ați făcut local, dacă doriți să le utilizați în viitor.",
|
||||
"reset-config-msg-l3": "Sunteți sigur că doriți să continuați?",
|
||||
"data-cleared-msg": "Datele au fost șterse cu succes",
|
||||
"actions-label": "Acțiuni",
|
||||
"copy-config-label": "Copiază Configurația",
|
||||
"data-copied-msg": "Configurația a fost copiată în clipboard",
|
||||
"reset-config-label": "Resetează Configurația",
|
||||
"css-save-btn": "Salvează Modificările",
|
||||
@@ -133,7 +128,6 @@
|
||||
"sign-in-tooltip": "Conectare",
|
||||
"sign-in-welcome": "Bună {username}!",
|
||||
"hide": "Ascunde",
|
||||
"open": "Deschide",
|
||||
"layout-masonry": "Zidărie",
|
||||
"language-label": "Limbă",
|
||||
"nav-links-tooltip": "Navigare și configurări",
|
||||
@@ -170,23 +164,11 @@
|
||||
"reset-toast": "Culorile Personalizate pentru {theme} au fost Eliminate"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Locația de Salvare",
|
||||
"location-local-label": "Aplică Local",
|
||||
"location-disk-label": "Scrie Modificările în Fișierul de Configurație",
|
||||
"save-button": "Salvează Modificările",
|
||||
"preview-button": "Previzualizează Modificările",
|
||||
"valid-label": "Configurația este Valabilă",
|
||||
"status-success-msg": "Sarcină Completată",
|
||||
"status-fail-msg": "Sarcină Eșuată",
|
||||
"success-msg-disk": "Fișierul de configurație a fost scris pe disc cu succes",
|
||||
"success-msg-local": "Modificările locale au fost salvate cu succes",
|
||||
"success-note-l1": "Aplicația ar trebui să se reconstruiască automat.",
|
||||
"success-note-l2": "Aceasta poate dura până la un minut.",
|
||||
"success-note-l3": "Va trebui să reîmprospătați pagina pentru ca modificările să aibă efect.",
|
||||
"error-msg-save-mode": "Vă rugăm să selectați un Mod de Salvare: Local sau Fișier",
|
||||
"error-msg-cannot-save": "A apărut o eroare la salvarea configurației",
|
||||
"error-msg-bad-json": "Eroare în JSON, posibil malformat",
|
||||
"warning-msg-validation": "Avertisment de Validare",
|
||||
"not-admin-note": "Nu puteți scrie modificările pe disc deoarece nu sunteți conectat ca administrator",
|
||||
"preview-applied-msg": "Configurația a fost previzualizată. Închide această fereastră pentru a vedea modificările.",
|
||||
"reset-confirm-msg": "Renunți la toate modificările nesalvate și revii la configurația originală?",
|
||||
@@ -215,7 +197,6 @@
|
||||
"intro-l3": "Pentru mai multe informații, vă rugăm să consultați",
|
||||
"intro-docs": "documentația",
|
||||
"backup-title-setup": "Creați un Backup",
|
||||
"backup-title-update": "Actualizați Backup-ul",
|
||||
"password-label-setup": "Alegeți o Parolă",
|
||||
"password-label-update": "Introduceți Parola",
|
||||
"backup-button-setup": "Backup",
|
||||
@@ -232,17 +213,6 @@
|
||||
"backup-success-msg": "Finalizat cu Succes",
|
||||
"restore-success-msg": "Configurația a fost Restaurată cu Succes"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Deschide În",
|
||||
"sametab": "Tab Curent",
|
||||
"newtab": "Tab Nou",
|
||||
"modal": "Modal Pop-Up",
|
||||
"workspace": "Vizualizare Spațiu de Lucru",
|
||||
"options-section-title": "Opțiuni",
|
||||
"edit-item": "Editare",
|
||||
"move-item": "Copiază sau Mută",
|
||||
"remove-item": "Șterge"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Deschide În",
|
||||
@@ -262,7 +232,6 @@
|
||||
"open-section": "Deschide Secțiunea",
|
||||
"edit-section": "Editare",
|
||||
"expand-collapse": "Extinde / Colapsează",
|
||||
"move-section": "Mută La",
|
||||
"remove-section": "Șterge",
|
||||
"section-options": "Opțiuni secțiune"
|
||||
}
|
||||
@@ -319,7 +288,6 @@
|
||||
"warning-msg-l3": "pentru a evita consecințele nedorite."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exportă Configurația",
|
||||
"copy-clipboard-btn": "Copiază în Clipboard",
|
||||
"copy-clipboard-tooltip": "Copiază toată configurația aplicației în clipboard-ul sistemului, în format YAML",
|
||||
"download-file-btn": "Descarcă ca Fișier",
|
||||
@@ -367,7 +335,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Se încarcă...",
|
||||
"show-more": "Extinde Detaliile",
|
||||
"cpu-details": "Detalii CPU",
|
||||
"mem-details": "Detalii Memorie",
|
||||
@@ -408,13 +375,9 @@
|
||||
"good-service-rest": "Servicii Bune pe Celelalte Linii"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Descărcare",
|
||||
"upload": "Încărcare",
|
||||
"downloaded": "Descărcat",
|
||||
"uploaded": "Încărcat",
|
||||
"remaining": "Rămas",
|
||||
"up": "Sus",
|
||||
"down": "Jos"
|
||||
"remaining": "Rămas"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "IP VPN",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Информация о Приложении",
|
||||
"support-dashy-link": "Contributions",
|
||||
"report-bug": "Сообщить об ошибке",
|
||||
"report-bug-description": "Если вы считаете, что нашли ошибку, пожалуйста",
|
||||
"report-bug-link": "сообщите о проблеме",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "Политикой Безопасности",
|
||||
"license": "Лицензия",
|
||||
"license-under": "Лицензировано под",
|
||||
"licence-third-party": "Лицензии на модули сторонних производителей см.",
|
||||
"licence-third-party-link": "Legal",
|
||||
"list-contributors": "Чтобы посмотреть полный список авторов и благодарностей, см.",
|
||||
"list-contributors-link": "Credits",
|
||||
"version": "Версия"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Вы должны сначала сделать резервную копию любых изменений, которые вы внесли локально, если вы хотите использовать их в будущем.",
|
||||
"reset-config-msg-l3": "Вы уверены, что хотите продолжить?",
|
||||
"data-cleared-msg": "Данные успешно очищены",
|
||||
"actions-label": "Действия",
|
||||
"copy-config-label": "Скопировать конфигурацию",
|
||||
"data-copied-msg": "Конфиг скопирован в буфер обмена",
|
||||
"reset-config-label": "Сбросить конфигурацию",
|
||||
"css-save-btn": "Сохранить изменения",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Выход",
|
||||
"sign-in-tooltip": "Авторизоваться",
|
||||
"sign-in-welcome": "Здравствуйте, {username}!",
|
||||
"hide": "Скрыть",
|
||||
"open": "Открыть"
|
||||
"hide": "Скрыть"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy версия",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "Пользовательские цвета для темы {theme} удалены"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Место сохранения",
|
||||
"location-local-label": "Применить локально",
|
||||
"location-disk-label": "Записать изменения в файл конфигурации на диск",
|
||||
"save-button": "Сохранить изменения",
|
||||
"preview-button": "Предпросмотр",
|
||||
"valid-label": "Конфигурация верна",
|
||||
"status-success-msg": "Задача завершена",
|
||||
"status-fail-msg": "Задача была провалена",
|
||||
"success-msg-disk": "Файл конфигурации успешно записан на диск",
|
||||
"success-msg-local": "Локальные изменения успешно сохранены",
|
||||
"success-note-l1": "Приложение должно перестроиться автоматически.",
|
||||
"success-note-l2": "Это может занять до минуты.",
|
||||
"success-note-l3": "Вам нужно будет обновить страницу, чтобы изменения вступили в силу.",
|
||||
"error-msg-save-mode": "Пожалуйста, выберите режим сохранения: локальный или файл",
|
||||
"error-msg-cannot-save": "Произошла ошибка при сохранении конфигурации",
|
||||
"error-msg-bad-json": "Ошибка в JSON, возможно, неверный формат",
|
||||
"warning-msg-validation": "Предупреждение валидатора",
|
||||
"not-admin-note": "Вы не можете записать измененные на диск, потому что вы не вошли в систему как администратор"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "Для получения дополнительной информации см.",
|
||||
"intro-docs": "документацию",
|
||||
"backup-title-setup": "Создать резервную копию",
|
||||
"backup-title-update": "Обновить резервную копию",
|
||||
"password-label-setup": "Выберите пароль",
|
||||
"password-label-update": "Введите ваш пароль",
|
||||
"backup-button-setup": "Создать резервную копию",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Успешно завершено",
|
||||
"restore-success-msg": "Конфигурация успешно восстановлена"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Открыть в",
|
||||
"sametab": "Текущей вкладке",
|
||||
"newtab": "Новой вкладке",
|
||||
"modal": "Всплывающем окне",
|
||||
"workspace": "Рабочей области",
|
||||
"options-section-title": "Настройки",
|
||||
"edit-item": "Редактировать",
|
||||
"move-item": "Скопировать или переместить",
|
||||
"remove-item": "Удалить"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Открыть в",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Открыть секцию",
|
||||
"edit-section": "Редактировать",
|
||||
"expand-collapse": "Раскрыть / Свернуть",
|
||||
"move-section": "Переместить в",
|
||||
"remove-section": "Удалить"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": "для избежания непредвиденных последствий."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Экспорт конфигурации",
|
||||
"copy-clipboard-btn": "Скопировано в буфер обмена",
|
||||
"copy-clipboard-tooltip": "Скопировать всю конфигурацию приложения, в формате YAML",
|
||||
"download-file-btn": "Скачать как файл",
|
||||
@@ -293,7 +261,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Загрузка...",
|
||||
"show-more": "Развернуть сведения",
|
||||
"cpu-details": "Сведения о ЦПУ",
|
||||
"mem-details": "Сведения о памяти",
|
||||
@@ -334,13 +301,9 @@
|
||||
"good-service-rest": "Хорошее обслуживание на всех остальных линиях"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Скачивание",
|
||||
"upload": "Загрузка",
|
||||
"downloaded": "Скачано",
|
||||
"uploaded": "Загружено",
|
||||
"remaining": "Остаётся",
|
||||
"up": "Вверх",
|
||||
"down": "Вниз"
|
||||
"remaining": "Остаётся"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
"reset-config-msg-l2": "Všetky zmeny, ktoré ste vykonali lokálne, by ste si mali najskôr zálohovať, ak ich chcete použiť v budúcnosti.",
|
||||
"reset-config-msg-l3": "Naozaj chcete pokračovať?",
|
||||
"data-cleared-msg": "Údaje boli úspešne vymazané",
|
||||
"actions-label": "Akcie",
|
||||
"copy-config-label": "Kopírovať konfig",
|
||||
"data-copied-msg": "Konfigurácia bola skopírovaná do schránky",
|
||||
"reset-config-label": "Obnoviť konfiguráciu",
|
||||
"css-save-btn": "Uložiť zmeny",
|
||||
@@ -110,23 +108,11 @@
|
||||
"reset-toast": "Vlastné farby pre {theme} boli odstránené"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Uložiť umiestnenie",
|
||||
"location-local-label": "Aplikovať lokálne",
|
||||
"location-disk-label": "Zapíšte zmeny do konfiguračného súboru",
|
||||
"save-button": "Uložiť zmeny",
|
||||
"preview-button": "Ukážka zmien",
|
||||
"valid-label": "Konfigurácia je platná",
|
||||
"status-success-msg": "Úloha dokončená",
|
||||
"status-fail-msg": "Úloha zlyhala",
|
||||
"success-msg-disk": "Konfiguračný súbor bol úspešne zapísaný na disk",
|
||||
"success-msg-local": "Miestne zmeny boli úspešne uložené",
|
||||
"success-note-l1": "Aplikácia by sa mala automaticky obnoviť.",
|
||||
"success-note-l2": "Môže to trvať až minútu.",
|
||||
"success-note-l3": "Aby sa zmeny prejavili, budete musieť obnoviť stránku.",
|
||||
"error-msg-save-mode": "Vyberte režim uloženia: Miestny alebo Súbor",
|
||||
"error-msg-cannot-save": "Pri ukladaní konfigurácie sa vyskytla chyba",
|
||||
"error-msg-bad-json": "Chyba v JSON, možno má nesprávny formát",
|
||||
"warning-msg-validation": "Upozornenie na validáciu",
|
||||
"not-admin-note": "Nemôžete zapisovať zmeneny na disk, pretože nie ste prihlásený ako správca"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -135,7 +121,6 @@
|
||||
"intro-l2": "Všetky údaje sú úplne end-to-end šifrované pomocou AES, pričom ako kľúč sa používa vaše heslo.",
|
||||
"intro-l3": "Viac informácií nájdete na",
|
||||
"backup-title-setup": "Vytvoriť zálohu",
|
||||
"backup-title-update": "Aktualizovať zálohu",
|
||||
"password-label-setup": "Vyberte si heslo",
|
||||
"password-label-update": "Zadajte svoje heslo",
|
||||
"backup-button-setup": "Záloha",
|
||||
@@ -152,17 +137,6 @@
|
||||
"backup-success-msg": "Úspešne dokončené",
|
||||
"restore-success-msg": "Konfigurácia bola úspešne obnovená"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Otvoriť v",
|
||||
"sametab": "Aktuálna karta",
|
||||
"newtab": "Nová karta",
|
||||
"modal": "Vyskakovací modal",
|
||||
"workspace": "Zobrazenie pracovného priestoru",
|
||||
"options-section-title": "Možnosti",
|
||||
"edit-item": "Upraviť",
|
||||
"move-item": "Kopírovať alebo presunúť",
|
||||
"remove-item": "Odstrániť"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Otvoriť v",
|
||||
@@ -181,7 +155,6 @@
|
||||
"open-section": "Otvorená sekcia",
|
||||
"edit-section": "Upraviť",
|
||||
"expand-collapse": "Rozbaliť / zbaliť",
|
||||
"move-section": "Presunúť do",
|
||||
"remove-section": "Odstrániť"
|
||||
}
|
||||
},
|
||||
@@ -232,7 +205,6 @@
|
||||
"warning-msg-l3": "aby sa predišlo neúmyselným následkom."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exportovať konfiguráciu",
|
||||
"copy-clipboard-btn": "Kopírovať do schránky",
|
||||
"copy-clipboard-tooltip": "Kopírovať všetky konfigurácie aplikácie do systémovej schránky vo formáte YAML",
|
||||
"download-file-btn": "Stiahnuť ako súbor",
|
||||
@@ -241,7 +213,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Načítavam...",
|
||||
"show-more": "Rozbaliť podrobnosti",
|
||||
"show-less": "Zobraziť menej",
|
||||
"open-link": "Pokračovať v čítaní"
|
||||
@@ -280,13 +251,9 @@
|
||||
"good-service-rest": "Dobré služby na všetkých ostatných linkách"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Stiahnuť",
|
||||
"upload": "Nahrať",
|
||||
"downloaded": "Stiahnuté",
|
||||
"uploaded": "Nahrané",
|
||||
"remaining": "Zostávajúce",
|
||||
"up": "Aktívne",
|
||||
"down": "Neaktívne"
|
||||
"remaining": "Zostávajúce"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "IP VPN",
|
||||
@@ -301,7 +268,6 @@
|
||||
"nextcloud": {
|
||||
"active": "active",
|
||||
"and": "a",
|
||||
"aplications": "aplikácie",
|
||||
"available": "dostupné",
|
||||
"away": "Preč",
|
||||
"cache-full": "MEDZIPAMÄTE PLNÉ",
|
||||
|
||||
@@ -50,8 +50,6 @@
|
||||
"reset-config-msg-l2": "Če želeti spremembe, ki ste jih naredili lokalno uporabiti v prihodnosti, ustvarite varnostno kopijo.",
|
||||
"reset-config-msg-l3": "Ali ste prepričani, da želite nadaljevati?",
|
||||
"data-cleared-msg": "Podatki so bili uspešno izbrisani",
|
||||
"actions-label": "Dejanja",
|
||||
"copy-config-label": "Kopiraj Konfiguracijo",
|
||||
"data-copied-msg": "Config je bil kopiran v odložišče",
|
||||
"reset-config-label": "Ponastavi Konfiguracijo",
|
||||
"css-save-btn": "Shrani spremembe",
|
||||
@@ -108,23 +106,11 @@
|
||||
"reset-toast": "Barve po Meri za {theme} Odstranjene"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Način Shranjevanja",
|
||||
"location-local-label": "Shrani Lokalno",
|
||||
"location-disk-label": "Zapišite spremembe v datoteko za konfiguracijo",
|
||||
"save-button": "Shrani Spremembe",
|
||||
"preview-button": "Predogled Sprememb",
|
||||
"valid-label": "Konfiguracija je veljavna",
|
||||
"status-success-msg": "Operacija dokončana",
|
||||
"status-fail-msg": "Operacija ni uspela",
|
||||
"success-msg-disk": "Konfiguracijska datoteka je uspešno zapisana na disk",
|
||||
"success-msg-local": "Lokalne spremembe so bile uspešno shranjene",
|
||||
"success-note-l1": "Aplikacija se bo samodejno obnovila.",
|
||||
"success-note-l2": "To lahko traja do ene minute.",
|
||||
"success-note-l3": "Za uveljavitev sprememb boste morali osvežiti stran.",
|
||||
"error-msg-save-mode": "Izberite način shranjevanja: Lokalno ali v Datoteko",
|
||||
"error-msg-cannot-save": "Pri shranjevanju konfiguracije je prišlo do napake",
|
||||
"error-msg-bad-json": "Napaka v JSON -u, morda nepravilno oblikovana",
|
||||
"warning-msg-validation": "Opozorilo o Validaciji",
|
||||
"not-admin-note": "Ne morete zapisati spremenjenega na disk, ker niste prijavljeni kot skrbnik"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -133,7 +119,6 @@
|
||||
"intro-l2": "Vsi podatki so v celoti šifrirani z AES, pri čemer je vaše geslo ključ.",
|
||||
"intro-l3": "Za več informacij si oglejte",
|
||||
"backup-title-setup": "Ustvari Varnostno Kopijo",
|
||||
"backup-title-update": "Posodobi Varnostno Kopijo",
|
||||
"password-label-setup": "Izberi Geslo",
|
||||
"password-label-update": "Vnesite Geslo",
|
||||
"backup-button-setup": "Varnosto Kopiraj",
|
||||
@@ -150,17 +135,6 @@
|
||||
"backup-success-msg": "Uspešno Zaključeno",
|
||||
"restore-success-msg": "Konfiguracija Uspešno Obnovljena"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Odpri V",
|
||||
"sametab": "Odpri v Trenutnem Zavihku",
|
||||
"newtab": "Odpri v Novem Zavihku",
|
||||
"modal": "Odpri v Pojavnem Oknu",
|
||||
"workspace": "Odpri v Delovnem Pogledu",
|
||||
"options-section-title": "Nastavitve",
|
||||
"edit-item": "Uredi",
|
||||
"move-item": "Kopiral ali Premakni",
|
||||
"remove-item": "Odstrani"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Odpri V",
|
||||
@@ -176,7 +150,6 @@
|
||||
"section": {
|
||||
"open-section": "Odpri Razdelek",
|
||||
"edit-section": "Uredi",
|
||||
"move-section": "Premakni v",
|
||||
"remove-section": "Odstrani"
|
||||
}
|
||||
},
|
||||
@@ -221,7 +194,6 @@
|
||||
"warning-msg-l3": "da bi se izognili neželenim posledicam."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Izvozi Nastavitve",
|
||||
"copy-clipboard-btn": "Kopirati v Odložišče",
|
||||
"copy-clipboard-tooltip": "Kopirajte vso konfiguracijo aplikacije v sistemsko odložišče v formatu YAML",
|
||||
"download-file-btn": "Prenesi kot Datoteko",
|
||||
|
||||
@@ -55,8 +55,6 @@
|
||||
"reset-config-msg-l2": "Du bör först göra en säkerhetskopia av alla ändringar du har gjort lokalt, om du vill använda dem i framtiden.",
|
||||
"reset-config-msg-l3": "Är du säker på att du vill fortsätta?",
|
||||
"data-cleared-msg": "Datarensning har lyckats",
|
||||
"actions-label": "Åtgärder",
|
||||
"copy-config-label": "Kopiera konfiguration",
|
||||
"data-copied-msg": "Konfiguration har kopierats till urklipp",
|
||||
"reset-config-label": "Återställ konfiguration",
|
||||
"css-save-btn": "Spara ändringar",
|
||||
@@ -86,8 +84,7 @@
|
||||
"sign-out-tooltip": "Logga ut",
|
||||
"sign-in-tooltip": "Logga in",
|
||||
"sign-in-welcome": "Hej {username}!",
|
||||
"hide": "Göm",
|
||||
"open": "Öppna"
|
||||
"hide": "Göm"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy-version",
|
||||
@@ -115,23 +112,11 @@
|
||||
"reset-toast": "Egna färger för {theme} har tagits bort"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Sparningsplats",
|
||||
"location-local-label": "Tillämpa lokalt",
|
||||
"location-disk-label": "Skriv ändringar till konfigurationsfil",
|
||||
"save-button": "Spara ändringar",
|
||||
"preview-button": "Förhandsgranska ändringar",
|
||||
"valid-label": "Konfigurationen är giltig",
|
||||
"status-success-msg": "Åtgärden slutfördes",
|
||||
"status-fail-msg": "Åtgärden misslyckades",
|
||||
"success-msg-disk": "Konfigurationsfil har skrivits till disk utan problem",
|
||||
"success-msg-local": "Lokala ändringar har sparats utan problem",
|
||||
"success-note-l1": "Återskapa",
|
||||
"success-note-l2": "Detta kan ta upp till en minut.",
|
||||
"success-note-l3": "Du måste uppdatera sidan för att ändringar ska gälla",
|
||||
"error-msg-save-mode": "Välj Lagringsläge: Lokalt eller Fil",
|
||||
"error-msg-cannot-save": "Ett fel uppstod när konfigurationen skulle sparas",
|
||||
"error-msg-bad-json": "Fel i JSON, möjligen felformaterat",
|
||||
"warning-msg-validation": "Valideringsvarning",
|
||||
"not-admin-note": "Du kan inte skriva ändringar till disk, eftersom du inte är inloggad som admin"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -140,7 +125,6 @@
|
||||
"intro-l2": "All data är fullständigt end-to-end krypterad med AES, med ditt lösenord som nyckel.",
|
||||
"intro-l3": "För mer information, vänligen se",
|
||||
"backup-title-setup": "Gör en säkerhetskopia",
|
||||
"backup-title-update": "Uppdatera säkerhetskopia",
|
||||
"password-label-setup": "Välj lösenord",
|
||||
"password-label-update": "Ange ditt lösenord",
|
||||
"backup-button-setup": "Säkerhetskopiering",
|
||||
@@ -157,17 +141,6 @@
|
||||
"backup-success-msg": "Slutfört utan problem",
|
||||
"restore-success-msg": "Konfigurationen har återställts utan problem"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Öppna i",
|
||||
"sametab": "Denna flik",
|
||||
"newtab": "Ny flik",
|
||||
"modal": "Pop-Up Modal",
|
||||
"workspace": "Workspace-vy",
|
||||
"options-section-title": "Alternativ",
|
||||
"edit-item": "Redigera",
|
||||
"move-item": "Kopiera eller flytta",
|
||||
"remove-item": "Ta bort"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Öppna i",
|
||||
@@ -186,7 +159,6 @@
|
||||
"open-section": "Öppna sektion",
|
||||
"edit-section": "Redigera",
|
||||
"expand-collapse": "Expandera / Minimera",
|
||||
"move-section": "Flytta till",
|
||||
"remove-section": "Ta bort"
|
||||
}
|
||||
},
|
||||
@@ -236,7 +208,6 @@
|
||||
"warning-msg-l3": "för att undvika oavsiktliga konsekvenser."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exportera konfiguration",
|
||||
"copy-clipboard-btn": "Kopiera till urklipp",
|
||||
"copy-clipboard-tooltip": "Kopiera alla appkonfigurationer till systemets urklipp i YAML-format",
|
||||
"download-file-btn": "Ladda ned som fil",
|
||||
@@ -245,7 +216,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Laddar...",
|
||||
"show-more": "Visa mer info",
|
||||
"show-less": "Visa mindre",
|
||||
"open-link": "Läs mer"
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Uygulama Bilgisi",
|
||||
"support-dashy-link": "Katkı sayfası",
|
||||
"report-bug": "Hata Bildir",
|
||||
"report-bug-description": "Bir hata bulduğunuzu düşünüyorsanız, lütfen",
|
||||
"report-bug-link": "bir Sorun bildirin",
|
||||
@@ -63,8 +62,6 @@
|
||||
"privacy-and-security-security-policy": "Güvenlik Politikası",
|
||||
"license": "Lisans",
|
||||
"license-under": "Şu lisans altında:",
|
||||
"licence-third-party": "Üçüncü taraf modüllerin lisansları için lütfen şuraya bakın:",
|
||||
"licence-third-party-link": "Yasal Bilgiler",
|
||||
"list-contributors": "Katkıda bulunanların tam listesi ve teşekkürler için şuraya bakın:",
|
||||
"list-contributors-link": "Katkıda Bulunanlar",
|
||||
"version": "Sürüm"
|
||||
@@ -90,8 +87,6 @@
|
||||
"reset-config-msg-l2": "Gelecekte kullanmak istiyorsanız, önce yerel olarak yaptığınız tüm değişiklikleri yedeklemelisiniz.",
|
||||
"reset-config-msg-l3": "Devam etmek istediğinizden emin misiniz?",
|
||||
"data-cleared-msg": "Veriler başarıyla temizlendi",
|
||||
"actions-label": "Eylemler",
|
||||
"copy-config-label": "Yapılandırmayı Kopyala",
|
||||
"data-copied-msg": "Yapılandırma panoya kopyalandı",
|
||||
"reset-config-label": "Yapılandırmayı Sıfırla",
|
||||
"css-save-btn": "Değişiklikleri Kaydet",
|
||||
@@ -125,8 +120,7 @@
|
||||
"sign-out-tooltip": "Çıkış Yap",
|
||||
"sign-in-tooltip": "Giriş Yap",
|
||||
"sign-in-welcome": "Merhaba {username}!",
|
||||
"hide": "Gizle",
|
||||
"open": "Aç"
|
||||
"hide": "Gizle"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy versiyon",
|
||||
@@ -154,23 +148,11 @@
|
||||
"reset-toast": "{theme} İçin Özel Renkler Kaldırıldı"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Konumu kaydet",
|
||||
"location-local-label": "Yerel Olarak Uygula",
|
||||
"location-disk-label": "Değişiklikleri Yapılandırma Dosyasına Yaz",
|
||||
"save-button": "Değişiklikleri Kaydet",
|
||||
"preview-button": "Değişiklikleri Önizle",
|
||||
"valid-label": "Yapılandırma Geçerli",
|
||||
"status-success-msg": "İşlem Tamamlandı",
|
||||
"status-fail-msg": "İşlem Başarısız",
|
||||
"success-msg-disk": "Yapılandırma dosyası diske başarıyla yazıldı",
|
||||
"success-msg-local": "Yerel değişiklikler başarıyla kaydedildi",
|
||||
"success-note-l1": "Değişikliklerin etkili olması için sayfayı yenilemeniz gerekecek.",
|
||||
"success-note-l2": "",
|
||||
"success-note-l3": "",
|
||||
"error-msg-save-mode": "Lütfen bir Kaydetme Modu seçin: Yerel veya Dosya",
|
||||
"error-msg-cannot-save": "Yapılandırma kaydedilirken bir hata oluştu",
|
||||
"error-msg-bad-json": "JSON'da hata, muhtemelen hatalı biçimlendirilmiş",
|
||||
"warning-msg-validation": "Doğrulama Uyarısı",
|
||||
"not-admin-note": "Yönetici olarak giriş yapmadığınız için değiştirilenleri diske yazamazsınız."
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -180,7 +162,6 @@
|
||||
"intro-l3": "Daha fazla bilgi için lütfen şuraya bakın:",
|
||||
"intro-docs": "belgeler",
|
||||
"backup-title-setup": "Yedekleme Yap",
|
||||
"backup-title-update": "Yedeği Güncelle",
|
||||
"password-label-setup": "Bir Şifre Seçin",
|
||||
"password-label-update": "Şifrenizi Girin",
|
||||
"backup-button-setup": "Yedekle",
|
||||
@@ -197,17 +178,6 @@
|
||||
"backup-success-msg": "Başarıyla tamamlandı",
|
||||
"restore-success-msg": "Yapılandırma Başarıyla Geri Yüklendi"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Şurada Aç",
|
||||
"sametab": "Mevcut Sekme",
|
||||
"newtab": "Yeni Sekme",
|
||||
"modal": "Açılır Pencere",
|
||||
"workspace": "Çalışma Alanı Görünümü",
|
||||
"options-section-title": "Seçenekler",
|
||||
"edit-item": "Düzenle",
|
||||
"move-item": "Kopyala veya Taşı",
|
||||
"remove-item": "Kaldır"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Şurada aç:",
|
||||
@@ -227,7 +197,6 @@
|
||||
"open-section": "Bölümü Aç",
|
||||
"edit-section": "Düzenle",
|
||||
"expand-collapse": "Genişlet / Daralt",
|
||||
"move-section": "Şuraya Taşı:",
|
||||
"remove-section": "Kaldır"
|
||||
}
|
||||
},
|
||||
@@ -278,7 +247,6 @@
|
||||
"warning-msg-l3": "istenmeyen sonuçlardan kaçınmak için."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Yapılandırmayı Dışarı Aktar",
|
||||
"copy-clipboard-btn": "Yapılandırmayı Panoya Kopyala",
|
||||
"copy-clipboard-tooltip": "Tüm uygulama yapılandırmasını sistem panosuna YAML biçiminde kopyalayın",
|
||||
"download-file-btn": "Dosya Olarak İndir",
|
||||
@@ -295,7 +263,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Yükleniyor...",
|
||||
"show-more": "Ayrıntıları Genişlet",
|
||||
"cpu-details": "CPU Detayları",
|
||||
"mem-details": "Bellek Detayları",
|
||||
@@ -336,13 +303,9 @@
|
||||
"good-service-rest": "Diğer Hatlarda İyi Hizmet"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "İndir",
|
||||
"upload": "Yükle",
|
||||
"downloaded": "İndirildi",
|
||||
"uploaded": "Yüklendi",
|
||||
"remaining": "Kalan",
|
||||
"up": "Yükleme",
|
||||
"down": "İndirme"
|
||||
"remaining": "Kalan"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "Інформація про програму",
|
||||
"support-dashy-link": "Сторінка внесків",
|
||||
"report-bug": "Повідомити про помилку",
|
||||
"report-bug-description": "Якщо ви вважаєте, що знайшли помилку, будь ласка",
|
||||
"report-bug-link": "повідомте про проблему",
|
||||
@@ -62,8 +61,6 @@
|
||||
"privacy-and-security-security-policy": "Політика безпеки",
|
||||
"license": "Ліцензія",
|
||||
"license-under": "Ліцензія згідно",
|
||||
"licence-third-party": "Ліцензії на сторонні модулі див.",
|
||||
"licence-third-party-link": "Юридична інформація",
|
||||
"list-contributors": "Повний список співавторів та подяки див.",
|
||||
"list-contributors-link": "Подяки",
|
||||
"version": "Версія"
|
||||
@@ -89,8 +86,6 @@
|
||||
"reset-config-msg-l2": "Ви повинні спочатку створити резервну копію будь-яких змін, які ви внесли локально, якщо ви хочете використовувати їх у майбутньому.",
|
||||
"reset-config-msg-l3": "Ви впевнені, що бажаєте продовжити?",
|
||||
"data-cleared-msg": "Дані успішно видалено",
|
||||
"actions-label": "Дії",
|
||||
"copy-config-label": "Копіювати налаштування",
|
||||
"data-copied-msg": "Налаштування скопійовано до буфера обміну",
|
||||
"reset-config-label": "Скинути налаштування",
|
||||
"css-save-btn": "Зберегти зміни",
|
||||
@@ -124,8 +119,7 @@
|
||||
"sign-out-tooltip": "Вийти",
|
||||
"sign-in-tooltip": "Увійти",
|
||||
"sign-in-welcome": "Вітаємо {username}!",
|
||||
"hide": "Приховати",
|
||||
"open": "Відкрити"
|
||||
"hide": "Приховати"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Версія Dashy",
|
||||
@@ -153,23 +147,11 @@
|
||||
"reset-toast": "Видалені користувацькі кольори для {theme}"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Місце збереження",
|
||||
"location-local-label": "Застосувати локально",
|
||||
"location-disk-label": "Записати зміни до файлу налаштування",
|
||||
"save-button": "Зберегти зміни",
|
||||
"preview-button": "Попередній перегляд змін",
|
||||
"valid-label": "Налаштування дійсне",
|
||||
"status-success-msg": "Завдання виконано",
|
||||
"status-fail-msg": "Не вдалося виконати завдання",
|
||||
"success-msg-disk": "Файл налаштування успішно записаний на диск",
|
||||
"success-msg-local": "Локальні зміни успішно збережено",
|
||||
"success-note-l1": "Програма повинна автоматично перебудуватися.",
|
||||
"success-note-l2": "Це може зайняти до хвилини.",
|
||||
"success-note-l3": "Вам потрібно буде оновити сторінку, щоб зміни набули чинності.",
|
||||
"error-msg-save-mode": "Будь ласка, виберіть режим збереження: локальний або файловий",
|
||||
"error-msg-cannot-save": "Під час збереження налаштування сталася помилка",
|
||||
"error-msg-bad-json": "Помилка у форматі JSON, можливо, некоректно сформований",
|
||||
"warning-msg-validation": "Попередження про перевірку",
|
||||
"not-admin-note": "Ви не можете записати зміни на диск, оскільки ви не увійшли як адміністратор"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -179,7 +161,6 @@
|
||||
"intro-l3": "Для отримання додаткової інформації див.",
|
||||
"intro-docs": "документи",
|
||||
"backup-title-setup": "Створити резервну копію",
|
||||
"backup-title-update": "Оновити резервну копію",
|
||||
"password-label-setup": "Оберіть пароль",
|
||||
"password-label-update": "Введіть свій пароль",
|
||||
"backup-button-setup": "Резервна копія",
|
||||
@@ -196,17 +177,6 @@
|
||||
"backup-success-msg": "Успішно завершено",
|
||||
"restore-success-msg": "Налаштування успішно відновлено"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Відкрити в",
|
||||
"sametab": "Поточна вкладка",
|
||||
"newtab": "Нова вкладка",
|
||||
"modal": "Модальне спливаюче вікно",
|
||||
"workspace": "Перегляд робочого простору",
|
||||
"options-section-title": "Параметри",
|
||||
"edit-item": "Редагувати",
|
||||
"move-item": "Копіювати або перемістити",
|
||||
"remove-item": "Видалити"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Відкрити в",
|
||||
@@ -225,7 +195,6 @@
|
||||
"open-section": "Відкрити розділ",
|
||||
"edit-section": "Редагувати",
|
||||
"expand-collapse": "Розгорнути / Згорнути",
|
||||
"move-section": "Перемістити до",
|
||||
"remove-section": "Видалити"
|
||||
}
|
||||
},
|
||||
@@ -276,7 +245,6 @@
|
||||
"warning-msg-l3": ", щоб уникнути небажаних наслідків."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Експорт налаштування",
|
||||
"copy-clipboard-btn": "Копіювати до буфера обміну",
|
||||
"copy-clipboard-tooltip": "Скопіювати всі налаштування програми до системного буфера обміну у форматі YAML",
|
||||
"download-file-btn": "Завантажити як файл",
|
||||
@@ -293,7 +261,6 @@
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Завантаження...",
|
||||
"show-more": "Розгорнути детальніше",
|
||||
"cpu-details": "Детальніше по ЦПУ",
|
||||
"mem-details": "Детальніше по пам'яті",
|
||||
@@ -334,13 +301,9 @@
|
||||
"good-service-rest": "Якісний сервіс на всіх інших лініях"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "Завантажити (Download)",
|
||||
"upload": "Вивантажити (Upload)",
|
||||
"downloaded": "Завантажено",
|
||||
"uploaded": "Вивантажено",
|
||||
"remaining": "Залишилося",
|
||||
"up": "Вгору",
|
||||
"down": "Вниз"
|
||||
"remaining": "Залишилося"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"general": {
|
||||
"close-modal": "关闭对话框",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"home": {
|
||||
"no-results": "找不到结果",
|
||||
"no-data": "没有配置数据",
|
||||
@@ -8,7 +13,8 @@
|
||||
"search-label": "搜索",
|
||||
"search-placeholder": "输入以筛选",
|
||||
"clear-search-tooltip": "清空搜索",
|
||||
"enter-to-search-web": "点击回车搜索"
|
||||
"enter-to-search-web": "点击回车搜索",
|
||||
"enter-to-open-url": "按回车键打开网址"
|
||||
},
|
||||
"splash-screen": {
|
||||
"loading": "加载中"
|
||||
@@ -46,9 +52,14 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "应用信息",
|
||||
"support-dashy-link": "贡献页面",
|
||||
"sponsor-heading": "赞助 Dashy",
|
||||
"sponsor-l1-prefix": "喜欢 Dashy 吗?请考虑",
|
||||
"sponsor-l1-link": "在 GitHub 上赞助我",
|
||||
"sponsor-l1-suffix": "以支持持续开发 🩷",
|
||||
"report-bug": "报告漏洞",
|
||||
"report-bug-description": "如果您认为发现了漏洞,请",
|
||||
"report-bug-debug-link": "查看调试菜单",
|
||||
"report-bug-middle": "并",
|
||||
"report-bug-link": "提交问题",
|
||||
"more-info": "更多信息",
|
||||
"source": "源代码",
|
||||
@@ -62,8 +73,6 @@
|
||||
"privacy-and-security-security-policy": "安全政策",
|
||||
"license": "许可证",
|
||||
"license-under": "根据许可证授权",
|
||||
"licence-third-party": "有关第三方模块的许可证,请参阅",
|
||||
"licence-third-party-link": "法律信息",
|
||||
"list-contributors": "查看所有贡献者和致谢的完整列表,请查看",
|
||||
"list-contributors-link": "致谢",
|
||||
"version": "版本"
|
||||
@@ -84,13 +93,12 @@
|
||||
"disabled-note": "您的管理员已禁用某些配置功能",
|
||||
"small-screen-note": "您正在使用非常小的屏幕,某些菜单屏幕可能不够优化",
|
||||
"app-info-button": "应用详情",
|
||||
"debug-info-button": "调试信息",
|
||||
"backup-note": "建议在进行更改之前备份你的配置。",
|
||||
"reset-config-msg-l1": "这将从本地存储中删除所有用户设置,但不会影响 conf.yml 文件。",
|
||||
"reset-config-msg-l2": "如果想在以后使用它们,应该首先备份你所做的任何更改。",
|
||||
"reset-config-msg-l3": "确定执行吗?",
|
||||
"data-cleared-msg": "成功清空数据",
|
||||
"actions-label": "行为",
|
||||
"copy-config-label": "复制设置",
|
||||
"data-copied-msg": "设置已保存在剪切板",
|
||||
"reset-config-label": "重置设置",
|
||||
"css-save-btn": "保存更改",
|
||||
@@ -109,30 +117,119 @@
|
||||
"workspace": "工作空间",
|
||||
"minimal": "最小化"
|
||||
},
|
||||
"remote-config": {
|
||||
"title": "加载远程配置?",
|
||||
"apply-button": "应用预览",
|
||||
"intro": "您正在从外部 URL 加载配置。是否继续?",
|
||||
"warning": "请确保在继续之前已阅读所加载的内容。一旦加载,您的浏览器将能够执行此配置中指定的任何客户端代码。",
|
||||
"field-title": "标题",
|
||||
"field-source": "来源",
|
||||
"field-sections": "分组",
|
||||
"field-items": "项目",
|
||||
"preview-note": "仅预览。这不会对磁盘做任何更改,也不会以任何方式影响您的 Dashy 实例。",
|
||||
"applied-toast": "已应用远程配置(仅预览)",
|
||||
"previewing-banner": "正在预览远程配置",
|
||||
"revert-button": "还原",
|
||||
"error-invalid-url": "远程配置 URL 无效(必须为 http 或 https)",
|
||||
"error-fetch": "无法获取远程配置。请检查 URL 是否可达,且允许 CORS。",
|
||||
"error-parse": "远程配置不是有效的 YAML",
|
||||
"error-not-config": "远程文件不像是 Dashy 配置",
|
||||
"error-apply": "应用远程配置失败"
|
||||
},
|
||||
"settings": {
|
||||
"theme-label": "主题",
|
||||
"layout-label": "布局",
|
||||
"layout-auto": "自动",
|
||||
"layout-horizontal": "水平",
|
||||
"layout-vertical": "垂直",
|
||||
"layout-masonry": "瀑布流",
|
||||
"item-size-label": "尺寸",
|
||||
"item-size-small": "小",
|
||||
"item-size-medium": "中",
|
||||
"item-size-large": "大",
|
||||
"language-label": "语言",
|
||||
"config-launcher-label": "设置",
|
||||
"config-launcher-tooltip": "更新设置",
|
||||
"nav-links-tooltip": "导航与配置",
|
||||
"toggle-nav-aria": "切换导航菜单",
|
||||
"sign-out-tooltip": "注销",
|
||||
"sign-in-tooltip": "登录",
|
||||
"sign-in-welcome": "你好 {username}!",
|
||||
"please-login": "登录以获得完整访问权限",
|
||||
"hide": "隐藏",
|
||||
"open": "打开"
|
||||
"options-title": "选项",
|
||||
"options-tooltip": "选项"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy 版本",
|
||||
"up-to-date": "已经是最新版了",
|
||||
"out-of-date": "存在可用更新",
|
||||
"unsupported-version-l1": "你使用的是不受支持的 Dashy 版本",
|
||||
"unsupported-version-l2": "为获得最佳体验和最近的安全补丁,请更新至"
|
||||
"unsupported-version-l2": "为获得最佳体验和最近的安全补丁,请更新至",
|
||||
"sw-update-available": "Dashy 有新版本可用。",
|
||||
"sw-update-action": "刷新"
|
||||
},
|
||||
"debug": {
|
||||
"getting-help": "获取帮助",
|
||||
"getting-help-msg": "如果您遇到问题,社区可以提供帮助。",
|
||||
"resources": {
|
||||
"troubleshooting-name": "故障排除",
|
||||
"troubleshooting-desc": "我们在此处概述了常见问题的解决方法",
|
||||
"docs-name": "文档",
|
||||
"docs-desc": "使用指南和配置说明",
|
||||
"source-name": "源代码",
|
||||
"source-desc": "或者,您可以从代码中确定问题",
|
||||
"email-name": "邮件支持",
|
||||
"email-note": "(仅限赞助者)",
|
||||
"email-desc": "给我留言,我会尽力提供帮助"
|
||||
},
|
||||
"reporting-bug": "报告漏洞",
|
||||
"reporting-bug-l1-prefix": "如果您发现某些功能未按预期工作,请",
|
||||
"reporting-bug-l1-link": "提交问题",
|
||||
"reporting-bug-l1-suffix": "至我们的 GitHub 仓库。",
|
||||
"reporting-bug-l2-intro": "提交漏洞报告时,请包含相关信息,例如:",
|
||||
"reporting-bug-list": {
|
||||
"version": "Dashy 版本",
|
||||
"deployment": "部署类型",
|
||||
"errors": "错误日志",
|
||||
"config": "配置片段",
|
||||
"overrides": "相关覆盖项",
|
||||
"environment": "环境信息"
|
||||
},
|
||||
"reporting-bug-note": "请务必删除任何敏感信息。如果工单未包含足够的信息用于调试、诊断和修复,可能会被关闭。由于时间有限,赞助者的支持和功能请求会被优先处理。",
|
||||
"app-version": "应用版本",
|
||||
"error-log": "错误日志",
|
||||
"error-log-hint-prefix": "请参阅",
|
||||
"error-log-hint-link": "此处",
|
||||
"error-log-hint-suffix": "以了解如何查看浏览器日志。服务端日志中可能也有有用的信息。",
|
||||
"no-errors": "本次会话未记录错误。",
|
||||
"current-config": "当前配置",
|
||||
"current-config-hint": "提交问题时,请包含您配置文件的相关部分。",
|
||||
"config-render-error": "无法渲染配置:{error}",
|
||||
"local-storage": "本地存储",
|
||||
"local-storage-hint": "保存在此浏览器中的设置。在清除之前,这些设置将覆盖默认值。",
|
||||
"no-local-storage": "本地未存储任何内容。",
|
||||
"local-storage-error": "无法读取本地存储:{error}",
|
||||
"environment": "环境",
|
||||
"environment-hint": "提交漏洞报告时附上的浏览器与设备信息。",
|
||||
"env": {
|
||||
"unknown": "未知",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"browser": "浏览器",
|
||||
"os": "操作系统",
|
||||
"viewport": "视口",
|
||||
"screen": "屏幕",
|
||||
"dpr": "像素比",
|
||||
"languages": "语言",
|
||||
"timezone": "时区",
|
||||
"colorScheme": "颜色方案",
|
||||
"reducedMotion": "减少动态效果",
|
||||
"online": "在线",
|
||||
"route": "当前路由",
|
||||
"mode": "构建模式",
|
||||
"swActive": "Service Worker"
|
||||
}
|
||||
},
|
||||
"language-switcher": {
|
||||
"title": "更改应用语言",
|
||||
@@ -153,24 +250,31 @@
|
||||
"reset-toast": "{theme} 的自定义颜色已经移除"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "保存位置",
|
||||
"location-local-label": "应用到本地",
|
||||
"location-disk-label": "将变更写入配置文件",
|
||||
"save-button": "保存变更",
|
||||
"preview-button": "预览更改",
|
||||
"valid-label": "设置有效",
|
||||
"status-success-msg": "任务完成",
|
||||
"status-fail-msg": "任务失败",
|
||||
"success-msg-disk": "配置文件保存成功",
|
||||
"success-msg-local": "本地变更保存成功",
|
||||
"success-note-l1": "应用需要自动重建",
|
||||
"success-note-l2": "这将持续一段时间",
|
||||
"success-note-l3": "你需要刷新页面以使变更生效",
|
||||
"error-msg-save-mode": "请选择保存位置:本地或者文件",
|
||||
"error-msg-cannot-save": "保存配置时出错",
|
||||
"error-msg-bad-json": "JSON 错误,可能格式错误",
|
||||
"warning-msg-validation": "验证警告",
|
||||
"not-admin-note": "不能保存变更到配置文件,因为你没有以管理员身份登录"
|
||||
"not-admin-note": "不能保存变更到配置文件,因为你没有以管理员身份登录",
|
||||
"preview-applied-msg": "配置已预览。关闭此弹窗以查看更改。",
|
||||
"reset-confirm-msg": "丢弃所有未保存的更改并还原到原始配置?",
|
||||
"wrap-label": "换行",
|
||||
"format-label": "格式化",
|
||||
"reset-label": "重置",
|
||||
"reset-tooltip": "丢弃所有编辑并还原到原始配置",
|
||||
"download-tooltip": "下载为 conf.yml",
|
||||
"download-label": "下载 YAML",
|
||||
"copy-tooltip": "复制 YAML 到剪贴板",
|
||||
"copy-label": "复制 YAML",
|
||||
"copy-success-msg": "配置已复制到剪贴板",
|
||||
"copy-fail-msg": "复制失败:{message}",
|
||||
"format-fail-msg": "无法格式化:{message}",
|
||||
"status-valid": "有效",
|
||||
"status-invalid": "无效",
|
||||
"status-warning": "{n} 个警告",
|
||||
"status-warnings": "{n} 个警告",
|
||||
"parse-fail-msg": "YAML 解析错误:{message}",
|
||||
"editor-init-fail-msg": "编辑器启动失败:{message}"
|
||||
},
|
||||
"cloud-sync": {
|
||||
"title": "云备份&恢复",
|
||||
@@ -179,7 +283,6 @@
|
||||
"intro-docs": "文档",
|
||||
"intro-l3": "有关更多信息,请参阅",
|
||||
"backup-title-setup": "创建备份",
|
||||
"backup-title-update": "更新备份",
|
||||
"password-label-setup": "选择密码",
|
||||
"password-label-update": "输入密码",
|
||||
"backup-button-setup": "备份",
|
||||
@@ -196,17 +299,6 @@
|
||||
"backup-success-msg": "备份完成",
|
||||
"restore-success-msg": "恢复完成"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "打开方式",
|
||||
"sametab": "在当前页打开",
|
||||
"newtab": "在新标签页打开",
|
||||
"modal": "在弹出窗口打开",
|
||||
"workspace": "在工作空间打开",
|
||||
"options-section-title": "选项",
|
||||
"edit-item": "编辑",
|
||||
"move-item": "复制或移动",
|
||||
"remove-item": "删除"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "打开方式",
|
||||
@@ -215,6 +307,7 @@
|
||||
"modal": "在弹出窗口打开",
|
||||
"workspace": "在工作空间打开",
|
||||
"clipboard": "复制到剪切板",
|
||||
"newwindow": "在新窗口打开",
|
||||
"options-section-title": "选项",
|
||||
"edit-item": "编辑",
|
||||
"move-item": "复制或移动",
|
||||
@@ -225,20 +318,24 @@
|
||||
"open-section": "打开 Section",
|
||||
"edit-section": "编辑",
|
||||
"expand-collapse": "展开 / 折叠",
|
||||
"move-section": "移动到",
|
||||
"remove-section": "删除"
|
||||
"remove-section": "删除",
|
||||
"section-options": "分组选项"
|
||||
}
|
||||
},
|
||||
"interactive-editor": {
|
||||
"menu": {
|
||||
"start-editing-tooltip": "进入交互编辑器",
|
||||
"config-unavailable": "配置编辑器不可用",
|
||||
"edit-site-data-subheading": "修改网站数据",
|
||||
"edit-config-as-code-btn": "以代码形式编辑配置",
|
||||
"edit-config-as-code-tooltip": "打开 YAML 编辑器以编辑原始配置文件",
|
||||
"edit-page-info-btn": "修改页面信息",
|
||||
"edit-page-info-tooltip": "应用标题、描述、导航链接、页尾文本等等。",
|
||||
"edit-app-config-btn": "修改应用设置",
|
||||
"edit-app-config-tooltip": "所有应用程序的其他设置选项",
|
||||
"edit-pages-btn": "修改页面",
|
||||
"edit-pages-tooltip": "添加或删除其他的视图",
|
||||
"edit-pages-subconfig-disabled": "页面仅能在根配置中编辑",
|
||||
"config-save-methods-subheading": "配置保存选项",
|
||||
"save-locally-btn": "暂存本地",
|
||||
"save-locally-tooltip": "将设置保存在浏览器上。这不会影响配置文件,仅作用于当前的浏览器。",
|
||||
@@ -252,6 +349,7 @@
|
||||
"edit-raw-config-tooltip": "通过 JSON 编辑器查看和修改原始配置",
|
||||
"cancel-changes-btn": "放弃修改",
|
||||
"cancel-changes-tooltip": "放弃当前修改,并退出编辑模式。这不会影响你已保存的配置。",
|
||||
"leave-while-editing-confirm": "您有未保存的更改。离开此页面并丢弃这些更改?",
|
||||
"edit-mode-name": "编辑模式",
|
||||
"edit-mode-subtitle": "你已进入编辑模式",
|
||||
"edit-mode-description": "你可以对配置进行修改并预览,在保存之前,你的任何更改都不会被保留。",
|
||||
@@ -268,6 +366,24 @@
|
||||
"edit-tooltip": "单击以修改,或右键点击以获取更多选项",
|
||||
"remove-confirm": "确定要删除此部分吗? 此操作可以稍后撤消。"
|
||||
},
|
||||
"edit-widget": {
|
||||
"edit-widget-title": "编辑小组件",
|
||||
"add-widget-title": "添加新小组件",
|
||||
"add-widget-btn": "添加小组件",
|
||||
"type-label": "小组件类型",
|
||||
"label-label": "标签",
|
||||
"update-interval-label": "更新间隔(秒)",
|
||||
"timeout-label": "超时(毫秒)",
|
||||
"use-proxy-label": "使用代理",
|
||||
"ignore-errors-label": "忽略错误",
|
||||
"options-heading": "小组件选项",
|
||||
"add-option-btn": "添加选项",
|
||||
"key-placeholder": "键",
|
||||
"value-placeholder": "值",
|
||||
"missing-type-err": "小组件类型是必需的",
|
||||
"remove-widget": "移除小组件",
|
||||
"remove-confirm": "确定要移除此小组件吗?此操作可以稍后撤消。"
|
||||
},
|
||||
"edit-app-config": {
|
||||
"warning-msg-title": "谨慎处理",
|
||||
"warning-msg-l1": "以下选项用于高级应用配置。",
|
||||
@@ -276,16 +392,43 @@
|
||||
"warning-msg-l3": "以避免意外的结果。"
|
||||
},
|
||||
"export": {
|
||||
"export-title": "导出配置",
|
||||
"copy-clipboard-btn": "复制到剪切板",
|
||||
"copy-clipboard-tooltip": "以YAML格式,将所有配置复制到剪切板 ",
|
||||
"download-file-btn": "下载配置文件",
|
||||
"download-file-tooltip": "以YAML文件格式,保存所有配置到本地"
|
||||
"download-file-tooltip": "以YAML文件格式,保存所有配置到本地",
|
||||
"current-config-title": "当前配置",
|
||||
"preview-toggle": "YAML 预览",
|
||||
"issues-title": "架构问题 ({n})",
|
||||
"config-list-title": "配置列表",
|
||||
"col-title": "标题",
|
||||
"col-path": "路径",
|
||||
"col-content": "内容",
|
||||
"col-status": "状态",
|
||||
"col-actions": "操作",
|
||||
"content-summary": "{sections} 个分组,{items} 个项目,{widgets} 个小组件",
|
||||
"status-loading": "加载中",
|
||||
"status-valid": "有效",
|
||||
"status-warnings": "{n} 个警告",
|
||||
"status-error": "错误",
|
||||
"status-unknown": "未知",
|
||||
"edit-current-btn": "编辑配置",
|
||||
"edit-current-tooltip": "在 JSON 编辑器中打开当前配置",
|
||||
"copy-fail-msg": "无法复制到剪贴板,请查看日志",
|
||||
"download-row-tooltip": "下载 YAML",
|
||||
"preview-row-tooltip": "切换 YAML 预览",
|
||||
"apply-row-tooltip": "应用配置"
|
||||
}
|
||||
},
|
||||
"critical-error": {
|
||||
"title": "配置加载错误",
|
||||
"subtitle": "由于配置错误,Dashy 未能正确加载。",
|
||||
"sub-ensure-that": "请确保",
|
||||
"sub-error-details": "错误详情",
|
||||
"sub-next-steps": "后续步骤",
|
||||
"ignore-button": "忽略严重错误"
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "加载中...",
|
||||
"show-more": "显示更多",
|
||||
"cpu-details": "CPU 详情",
|
||||
"mem-details": "内存 详情",
|
||||
@@ -326,13 +469,9 @@
|
||||
"good-service-rest": "所有其他线路正常"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "下载",
|
||||
"upload": "上传",
|
||||
"downloaded": "已下载",
|
||||
"uploaded": "已上传",
|
||||
"remaining": "剩余",
|
||||
"up": "上行",
|
||||
"down": "下行"
|
||||
"remaining": "剩余"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
@@ -342,7 +481,8 @@
|
||||
"post-code": "邮编",
|
||||
"location": "位置",
|
||||
"timezone": "时区",
|
||||
"organization": "组织"
|
||||
"organization": "组织",
|
||||
"forwarded-port": "转发端口"
|
||||
},
|
||||
"nextcloud": {
|
||||
"active": "激活",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"general": {
|
||||
"close-modal": "關閉對話框",
|
||||
"confirm": "確認",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"home": {
|
||||
"no-results": "沒有結果",
|
||||
"no-data": "未設定資料",
|
||||
@@ -8,7 +13,8 @@
|
||||
"search-label": "搜尋",
|
||||
"search-placeholder": "開始輸入以篩選",
|
||||
"clear-search-tooltip": "清空搜尋",
|
||||
"enter-to-search-web": "按下 Enter 鍵來搜尋網際網路"
|
||||
"enter-to-search-web": "按下 Enter 鍵來搜尋網際網路",
|
||||
"enter-to-open-url": "按下 Enter 鍵以開啟網址"
|
||||
},
|
||||
"splash-screen": {
|
||||
"loading": "正在載入"
|
||||
@@ -46,9 +52,14 @@
|
||||
},
|
||||
"app-info": {
|
||||
"title": "應用程式資訊",
|
||||
"support-dashy-link": "貢獻頁面",
|
||||
"sponsor-heading": "贊助 Dashy",
|
||||
"sponsor-l1-prefix": "喜歡 Dashy 嗎?請考慮",
|
||||
"sponsor-l1-link": "在 GitHub 上贊助我",
|
||||
"sponsor-l1-suffix": "以協助持續開發 🩷",
|
||||
"report-bug": "回報錯誤",
|
||||
"report-bug-description": "若您認為您找到錯誤,請",
|
||||
"report-bug-debug-link": "查看除錯選單",
|
||||
"report-bug-middle": "並",
|
||||
"report-bug-link": "提出問題",
|
||||
"more-info": "更多資訊",
|
||||
"source": "原始碼",
|
||||
@@ -62,8 +73,6 @@
|
||||
"privacy-and-security-security-policy": "安全政策",
|
||||
"license": "授權條款",
|
||||
"license-under": "授權條款為",
|
||||
"licence-third-party": "關於第三方模組的授權條款,請參閱",
|
||||
"licence-third-party-link": "法律聲明",
|
||||
"list-contributors": "關於貢獻和致謝者的完整名單,請參閱",
|
||||
"list-contributors-link": "致謝",
|
||||
"version": "版本"
|
||||
@@ -84,13 +93,12 @@
|
||||
"disabled-note": "某些設定功能被您的管理員停用",
|
||||
"small-screen-note": "您使用的裝置螢幕過小,此選單的某些畫面可能會跑版",
|
||||
"app-info-button": "App 詳細資訊",
|
||||
"debug-info-button": "除錯資訊",
|
||||
"backup-note": "建議在對您的設定做出變更之前先進行備份。",
|
||||
"reset-config-msg-l1": "這會移除所有的本機設定,但不會影響 ‘conf.yml’ 檔案。",
|
||||
"reset-config-msg-l2": "如果您未來還想使用您所做出的本機變更的話,請先備份。",
|
||||
"reset-config-msg-l3": "您確定要繼續嗎?",
|
||||
"data-cleared-msg": "已成功清空資料",
|
||||
"actions-label": "動作",
|
||||
"copy-config-label": "複製設定",
|
||||
"data-copied-msg": "設定已複製到剪貼簿",
|
||||
"reset-config-label": "重置設定",
|
||||
"css-save-btn": "儲存變更",
|
||||
@@ -109,30 +117,119 @@
|
||||
"workspace": "工作區",
|
||||
"minimal": "最小化"
|
||||
},
|
||||
"remote-config": {
|
||||
"title": "載入遠端設定?",
|
||||
"apply-button": "套用預覽",
|
||||
"intro": "您正在從外部網址載入設定。您要繼續嗎?",
|
||||
"warning": "請務必先閱讀您所載入的內容後再繼續。一旦載入,您的瀏覽器將可能執行此設定中所指定的任何用戶端程式碼。",
|
||||
"field-title": "標題",
|
||||
"field-source": "來源",
|
||||
"field-sections": "區塊",
|
||||
"field-items": "項目",
|
||||
"preview-note": "僅供預覽。這不會對硬碟做出任何變更,也不會以任何方式影響您的 Dashy 實例。",
|
||||
"applied-toast": "已套用遠端設定(僅預覽)",
|
||||
"previewing-banner": "正在預覽遠端設定",
|
||||
"revert-button": "還原",
|
||||
"error-invalid-url": "遠端設定的網址無效(必須為 http 或 https)",
|
||||
"error-fetch": "無法取得遠端設定。請確認網址可達,且已允許 CORS。",
|
||||
"error-parse": "遠端設定不是有效的 YAML",
|
||||
"error-not-config": "遠端檔案似乎不是 Dashy 設定",
|
||||
"error-apply": "套用遠端設定失敗"
|
||||
},
|
||||
"settings": {
|
||||
"theme-label": "主題",
|
||||
"layout-label": "版面配置",
|
||||
"layout-auto": "自動",
|
||||
"layout-horizontal": "水平",
|
||||
"layout-vertical": "垂直",
|
||||
"layout-masonry": "瀑布流",
|
||||
"item-size-label": "尺寸",
|
||||
"item-size-small": "小",
|
||||
"item-size-medium": "中",
|
||||
"item-size-large": "大",
|
||||
"language-label": "語言",
|
||||
"config-launcher-label": "設定",
|
||||
"config-launcher-tooltip": "更新設定",
|
||||
"nav-links-tooltip": "導覽與設定",
|
||||
"toggle-nav-aria": "切換導覽選單",
|
||||
"sign-out-tooltip": "登出",
|
||||
"sign-in-tooltip": "登入",
|
||||
"sign-in-welcome": "您好 {username}!",
|
||||
"please-login": "登入以取得完整存取權限",
|
||||
"hide": "隱藏",
|
||||
"open": "開啟"
|
||||
"options-title": "選項",
|
||||
"options-tooltip": "選項"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy 版本",
|
||||
"up-to-date": "已經是最新版本",
|
||||
"out-of-date": "有可用更新",
|
||||
"unsupported-version-l1": "您使用的是不受支援的 Dashy 版本",
|
||||
"unsupported-version-l2": "為獲得最佳體驗和最新的安全性更新,請更新至"
|
||||
"unsupported-version-l2": "為獲得最佳體驗和最新的安全性更新,請更新至",
|
||||
"sw-update-available": "Dashy 有新版本可用。",
|
||||
"sw-update-action": "重新整理"
|
||||
},
|
||||
"debug": {
|
||||
"getting-help": "取得協助",
|
||||
"getting-help-msg": "如果您遇到問題,社群可以提供協助。",
|
||||
"resources": {
|
||||
"troubleshooting-name": "疑難排解",
|
||||
"troubleshooting-desc": "我們在此列出了常見問題的解決方法",
|
||||
"docs-name": "說明文件",
|
||||
"docs-desc": "使用指南與設定說明",
|
||||
"source-name": "原始碼",
|
||||
"source-desc": "或者,您可以從程式碼中找出問題",
|
||||
"email-name": "電子郵件支援",
|
||||
"email-note": "(僅限贊助者)",
|
||||
"email-desc": "歡迎來信,我會盡力協助"
|
||||
},
|
||||
"reporting-bug": "回報錯誤",
|
||||
"reporting-bug-l1-prefix": "如果您發現某些功能未按預期運作,請",
|
||||
"reporting-bug-l1-link": "提出問題",
|
||||
"reporting-bug-l1-suffix": "至我們的 GitHub 儲存庫。",
|
||||
"reporting-bug-l2-intro": "提交錯誤報告時,請包含相關資訊,例如:",
|
||||
"reporting-bug-list": {
|
||||
"version": "Dashy 版本",
|
||||
"deployment": "部署類型",
|
||||
"errors": "錯誤紀錄",
|
||||
"config": "設定片段",
|
||||
"overrides": "相關覆寫項目",
|
||||
"environment": "環境資訊"
|
||||
},
|
||||
"reporting-bug-note": "請務必移除任何敏感資訊。若工單未包含足夠的資訊以進行除錯、診斷與修正,可能會被關閉。由於時間有限,贊助者的支援與功能請求會優先處理。",
|
||||
"app-version": "應用程式版本",
|
||||
"error-log": "錯誤紀錄",
|
||||
"error-log-hint-prefix": "請參閱",
|
||||
"error-log-hint-link": "此處",
|
||||
"error-log-hint-suffix": "以了解如何查看瀏覽器日誌。伺服器日誌中可能也包含有用的資訊。",
|
||||
"no-errors": "本次工作階段未記錄任何錯誤。",
|
||||
"current-config": "目前設定",
|
||||
"current-config-hint": "提出問題時,請包含您設定檔的相關部分。",
|
||||
"config-render-error": "無法呈現設定:{error}",
|
||||
"local-storage": "本機儲存空間",
|
||||
"local-storage-hint": "儲存在此瀏覽器中的設定。在清除之前,這些設定會覆蓋預設值。",
|
||||
"no-local-storage": "本機未儲存任何內容。",
|
||||
"local-storage-error": "無法讀取本機儲存空間:{error}",
|
||||
"environment": "環境",
|
||||
"environment-hint": "提交錯誤報告時要附上的瀏覽器與裝置資訊。",
|
||||
"env": {
|
||||
"unknown": "未知",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"browser": "瀏覽器",
|
||||
"os": "作業系統",
|
||||
"viewport": "可視區域",
|
||||
"screen": "螢幕",
|
||||
"dpr": "像素比",
|
||||
"languages": "語言",
|
||||
"timezone": "時區",
|
||||
"colorScheme": "色彩模式",
|
||||
"reducedMotion": "減少動態效果",
|
||||
"online": "線上",
|
||||
"route": "目前路由",
|
||||
"mode": "建置模式",
|
||||
"swActive": "Service Worker"
|
||||
}
|
||||
},
|
||||
"language-switcher": {
|
||||
"title": "更改應用程式語言",
|
||||
@@ -153,24 +250,31 @@
|
||||
"reset-toast": "{theme} 的自訂顏色已移除"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "儲存位置",
|
||||
"location-local-label": "套用到本機",
|
||||
"location-disk-label": "將變更寫入設定檔",
|
||||
"save-button": "儲存變更",
|
||||
"preview-button": "預覽變更",
|
||||
"valid-label": "設定有效",
|
||||
"status-success-msg": "工作完成",
|
||||
"status-fail-msg": "工作失敗",
|
||||
"success-msg-disk": "設定檔已成功寫入硬碟",
|
||||
"success-msg-local": "本機變更已儲存成功",
|
||||
"success-note-l1": "應用程式應會自動重新建置。",
|
||||
"success-note-l2": "可能會需要一分鐘",
|
||||
"success-note-l3": "您需要重新整理頁面以套用變更。",
|
||||
"error-msg-save-mode": "请選擇儲存位置:本機或者檔案",
|
||||
"error-msg-cannot-save": "儲存設定時發生錯誤",
|
||||
"error-msg-bad-json": "JSON 錯誤,可能是格式錯誤",
|
||||
"warning-msg-validation": "驗證警告",
|
||||
"not-admin-note": "您無法將變更寫入磁碟,因為您未以系統管理員身分登入"
|
||||
"not-admin-note": "您無法將變更寫入磁碟,因為您未以系統管理員身分登入",
|
||||
"preview-applied-msg": "已預覽設定。關閉此視窗以查看變更。",
|
||||
"reset-confirm-msg": "捨棄所有未儲存的變更並還原為原始設定?",
|
||||
"wrap-label": "換行",
|
||||
"format-label": "格式化",
|
||||
"reset-label": "重設",
|
||||
"reset-tooltip": "捨棄所有編輯內容並還原為原始設定",
|
||||
"download-tooltip": "下載為 conf.yml",
|
||||
"download-label": "下載 YAML",
|
||||
"copy-tooltip": "複製 YAML 到剪貼簿",
|
||||
"copy-label": "複製 YAML",
|
||||
"copy-success-msg": "設定已複製到剪貼簿",
|
||||
"copy-fail-msg": "複製失敗:{message}",
|
||||
"format-fail-msg": "無法格式化:{message}",
|
||||
"status-valid": "有效",
|
||||
"status-invalid": "無效",
|
||||
"status-warning": "{n} 個警告",
|
||||
"status-warnings": "{n} 個警告",
|
||||
"parse-fail-msg": "YAML 解析錯誤:{message}",
|
||||
"editor-init-fail-msg": "編輯器啟動失敗:{message}"
|
||||
},
|
||||
"cloud-sync": {
|
||||
"title": "雲端備份 & 復原",
|
||||
@@ -179,7 +283,6 @@
|
||||
"intro-l3": "若想了解更多資訊,請參考",
|
||||
"intro-docs": "說明文件",
|
||||
"backup-title-setup": "建立備份",
|
||||
"backup-title-update": "更新備份",
|
||||
"password-label-setup": "選擇密碼",
|
||||
"password-label-update": "輸入密碼",
|
||||
"backup-button-setup": "備份",
|
||||
@@ -196,17 +299,6 @@
|
||||
"backup-success-msg": "備份成功",
|
||||
"restore-success-msg": "設定已成功復原"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "以...開啟",
|
||||
"sametab": "目前分頁",
|
||||
"newtab": "新分頁",
|
||||
"modal": "彈出視窗",
|
||||
"workspace": "工作區",
|
||||
"options-section-title": "選項",
|
||||
"edit-item": "編輯",
|
||||
"move-item": "複製或移動",
|
||||
"remove-item": "移除"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "以...開啟",
|
||||
@@ -215,6 +307,7 @@
|
||||
"modal": "彈出視窗",
|
||||
"workspace": "工作區",
|
||||
"clipboard": "複製到剪貼簿",
|
||||
"newwindow": "新視窗",
|
||||
"options-section-title": "選項",
|
||||
"edit-item": "編輯",
|
||||
"move-item": "複製或移動",
|
||||
@@ -225,20 +318,24 @@
|
||||
"open-section": "開啟區域",
|
||||
"edit-section": "編輯",
|
||||
"expand-collapse": "展開 / 收合",
|
||||
"move-section": "移動至",
|
||||
"remove-section": "移除"
|
||||
"remove-section": "移除",
|
||||
"section-options": "區塊選項"
|
||||
}
|
||||
},
|
||||
"interactive-editor": {
|
||||
"menu": {
|
||||
"start-editing-tooltip": "進入互動式編輯器",
|
||||
"config-unavailable": "設定編輯器無法使用",
|
||||
"edit-site-data-subheading": "編輯網站資料",
|
||||
"edit-config-as-code-btn": "以程式碼形式編輯設定",
|
||||
"edit-config-as-code-tooltip": "開啟 YAML 編輯器以編輯原始設定檔",
|
||||
"edit-page-info-btn": "編輯頁面資訊",
|
||||
"edit-page-info-tooltip": "應用程式標題、說明、導航列連結、頁尾文字...等",
|
||||
"edit-app-config-btn": "編輯應用程式設定",
|
||||
"edit-app-config-tooltip": "其餘應用程式設定選項",
|
||||
"edit-pages-btn": "編輯頁面",
|
||||
"edit-pages-tooltip": "新增或移除額外檢視",
|
||||
"edit-pages-subconfig-disabled": "頁面僅能從根層級設定中編輯",
|
||||
"config-save-methods-subheading": "設定儲存選項",
|
||||
"save-locally-btn": "本機儲存",
|
||||
"save-locally-tooltip": "將設定儲存到瀏覽器儲存空間中。這不會影響到您的設定檔,但變更僅會儲存在此裝置",
|
||||
@@ -252,6 +349,7 @@
|
||||
"edit-raw-config-tooltip": "透過 JSON 編輯器檢視或編輯原始設定",
|
||||
"cancel-changes-btn": "取消編輯",
|
||||
"cancel-changes-tooltip": "重設目前的更動,並退出編輯模式。這不會影響您已儲存的設定",
|
||||
"leave-while-editing-confirm": "您有未儲存的變更。離開此頁面並捨棄變更?",
|
||||
"edit-mode-name": "編輯模式",
|
||||
"edit-mode-subtitle": "目前為編輯模式",
|
||||
"edit-mode-description": "這代表您可以編輯設定並預覽。但在您儲存前,變更將不被儲存。",
|
||||
@@ -268,6 +366,24 @@
|
||||
"edit-tooltip": "點擊以編輯,右鍵以查看更多選項",
|
||||
"remove-confirm": "您確定要移除此區塊嗎?此操作可復原。"
|
||||
},
|
||||
"edit-widget": {
|
||||
"edit-widget-title": "編輯小工具",
|
||||
"add-widget-title": "新增小工具",
|
||||
"add-widget-btn": "新增小工具",
|
||||
"type-label": "小工具類型",
|
||||
"label-label": "標籤",
|
||||
"update-interval-label": "更新間隔(秒)",
|
||||
"timeout-label": "逾時(毫秒)",
|
||||
"use-proxy-label": "使用代理",
|
||||
"ignore-errors-label": "忽略錯誤",
|
||||
"options-heading": "小工具選項",
|
||||
"add-option-btn": "新增選項",
|
||||
"key-placeholder": "鍵",
|
||||
"value-placeholder": "值",
|
||||
"missing-type-err": "必須選擇小工具類型",
|
||||
"remove-widget": "移除小工具",
|
||||
"remove-confirm": "您確定要移除此小工具嗎?此操作可復原。"
|
||||
},
|
||||
"edit-app-config": {
|
||||
"warning-msg-title": "謹慎操作",
|
||||
"warning-msg-l1": "下列選項用於進階應用程式設定。",
|
||||
@@ -276,17 +392,46 @@
|
||||
"warning-msg-l3": "以避免意外後果。"
|
||||
},
|
||||
"export": {
|
||||
"export-title": "匯出設定",
|
||||
"copy-clipboard-btn": "複製至剪貼簿",
|
||||
"copy-clipboard-tooltip": "將所有應用程式設定以 YAML 格式複製到系統剪貼簿。",
|
||||
"download-file-btn": "下載檔案",
|
||||
"download-file-tooltip": "將所有應用程式設定以 YAML 檔案下載到您的裝置。"
|
||||
"download-file-tooltip": "將所有應用程式設定以 YAML 檔案下載到您的裝置。",
|
||||
"current-config-title": "目前設定",
|
||||
"preview-toggle": "YAML 預覽",
|
||||
"issues-title": "結構描述問題 ({n})",
|
||||
"config-list-title": "設定清單",
|
||||
"col-title": "標題",
|
||||
"col-path": "路徑",
|
||||
"col-content": "內容",
|
||||
"col-status": "狀態",
|
||||
"col-actions": "操作",
|
||||
"content-summary": "{sections} 個區塊,{items} 個項目,{widgets} 個小工具",
|
||||
"status-loading": "載入中",
|
||||
"status-valid": "有效",
|
||||
"status-warnings": "{n} 個警告",
|
||||
"status-error": "錯誤",
|
||||
"status-unknown": "未知",
|
||||
"edit-current-btn": "編輯設定",
|
||||
"edit-current-tooltip": "在 JSON 編輯器中開啟目前設定",
|
||||
"copy-fail-msg": "無法複製到剪貼簿,請查看記錄",
|
||||
"download-row-tooltip": "下載 YAML",
|
||||
"preview-row-tooltip": "切換 YAML 預覽",
|
||||
"apply-row-tooltip": "套用設定"
|
||||
}
|
||||
},
|
||||
"critical-error": {
|
||||
"title": "設定載入錯誤",
|
||||
"subtitle": "由於設定錯誤,Dashy 無法正確載入。",
|
||||
"sub-ensure-that": "請確認",
|
||||
"sub-error-details": "錯誤詳情",
|
||||
"sub-next-steps": "後續步驟",
|
||||
"ignore-button": "忽略嚴重錯誤"
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "正在載入...",
|
||||
"show-more": "展開詳細資訊",
|
||||
"cpu-details": "CPU 詳細資訊",
|
||||
"mem-details": "記憶體詳細資訊",
|
||||
"show-less": "顯示較少",
|
||||
"open-link": "繼續閱讀"
|
||||
},
|
||||
@@ -324,13 +469,9 @@
|
||||
"good-service-rest": "所有其他路線正常營運"
|
||||
},
|
||||
"synology-download": {
|
||||
"download": "下載",
|
||||
"upload": "上傳",
|
||||
"downloaded": "已下載",
|
||||
"uploaded": "已上傳",
|
||||
"remaining": "剩餘",
|
||||
"up": "上傳",
|
||||
"down": "下載"
|
||||
"remaining": "剩餘"
|
||||
},
|
||||
"gluetun-status": {
|
||||
"vpn-ip": "VPN IP",
|
||||
@@ -340,7 +481,8 @@
|
||||
"post-code": "郵遞區號",
|
||||
"location": "地區",
|
||||
"timezone": "時區",
|
||||
"organization": "組織"
|
||||
"organization": "組織",
|
||||
"forwarded-port": "轉送連接埠"
|
||||
},
|
||||
"nextcloud": {
|
||||
"active": "啟用",
|
||||
|
||||
@@ -51,21 +51,10 @@
|
||||
"copied-toast": "Theme data for {theme} copied t' ye clipboard"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Save Location",
|
||||
"location-local-label": "Apply Locally",
|
||||
"location-disk-label": "Write Changes to Config File",
|
||||
"save-button": "Save Changes",
|
||||
"valid-label": "Config is Valid",
|
||||
"status-success-msg": "Task Complete",
|
||||
"status-fail-msg": "Task Failed",
|
||||
"success-msg-disk": "Th' config file written to disk successfully",
|
||||
"success-msg-local": "Ye local changes were successfully saved",
|
||||
"success-note-l1": "th' app should rebuild automatically.",
|
||||
"success-note-l2": "This may take up t' a minute.",
|
||||
"success-note-l3": "ye will need t' refresh th' page fer changes t' take effect.",
|
||||
"error-msg-cannot-save": "An error occurred savin' config",
|
||||
"error-msg-bad-json": "Error in ye JSON, possibly malformed",
|
||||
"warning-msg-validation": "Validation Warnin' Ahead",
|
||||
"not-admin-note": "ye cannot write changed t' disk, because ye be not logged in as an admin"
|
||||
},
|
||||
"cloud-sync": {
|
||||
@@ -73,7 +62,6 @@
|
||||
"intro-l2": "All data be fully end-t'-end encrypted with AES, usin' yer password as th' key.",
|
||||
"intro-l3": "For more info, please see th'",
|
||||
"backup-title-setup": "Make ye Backup",
|
||||
"backup-title-update": "Update ye Backup",
|
||||
"password-label-setup": "Choose ye Password",
|
||||
"password-label-update": "Enter yer Password",
|
||||
"backup-id-label": "Yer Backup ID",
|
||||
@@ -81,11 +69,5 @@
|
||||
"backup-missing-password": "Missin'g' Password",
|
||||
"backup-error-unknown": "Unable t' process request",
|
||||
"backup-error-password": "Incorrect password. Walk the plank! Please enter yer current password."
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Stay Aboard",
|
||||
"newtab": "Walk the Plank",
|
||||
"modal": "Open in ye Pop-Up Ship",
|
||||
"workspace": "Open on Workspace Deck"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,11 +127,13 @@ export default {
|
||||
<style scoped lang="scss">
|
||||
|
||||
.language-switcher {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
height: calc(100% - 2rem);
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
position: relative;
|
||||
h3.title {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -153,8 +155,8 @@ export default {
|
||||
position: absolute;
|
||||
margin: 1rem auto;
|
||||
cursor: default;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
width: inherit;
|
||||
}
|
||||
&.mini {
|
||||
height: auto;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="iframe-widget">
|
||||
<iframe
|
||||
v-if="frameUrl"
|
||||
:key="updateCount"
|
||||
:src="frameUrl"
|
||||
:id="frameId"
|
||||
title="Iframe Widget"
|
||||
@@ -27,7 +28,7 @@ export default {
|
||||
this.error('Iframe widget expects a URL');
|
||||
return null;
|
||||
}
|
||||
return `${usersChoice}${this.updatePathParam}`;
|
||||
return usersChoice;
|
||||
},
|
||||
frameHeight() {
|
||||
return this.options.frameHeight;
|
||||
@@ -36,26 +37,12 @@ export default {
|
||||
frameId() {
|
||||
return `iframe-${btoa(this.frameUrl || 'empty').substring(0, 16)}`;
|
||||
},
|
||||
/* Generate a URL param, to be updated in order to re-fetch image */
|
||||
updatePathParam() {
|
||||
return this.updateCount ? `#dashy-update-${this.updateCount}` : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Refreshes iframe contents, called by parent */
|
||||
update() {
|
||||
this.startLoading();
|
||||
const iframe = document.getElementById(this.frameId);
|
||||
if (iframe.contentWindow) {
|
||||
try {
|
||||
iframe.contentWindow.location.href = this.frameUrl;
|
||||
iframe.contentWindow.location.reload(true);
|
||||
} catch (e) {
|
||||
this.error('Failed to refresh iframe', e, true);
|
||||
}
|
||||
} else {
|
||||
this.error('Couldn\'t find iframe', null, true);
|
||||
}
|
||||
this.updateCount += 1;
|
||||
this.finishLoading();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -47,11 +47,16 @@ export default {
|
||||
},
|
||||
endpoint() {
|
||||
const apiKey = this.parseAsEnvVar(this.options.apiKey);
|
||||
const { city, lat, lon } = this.options;
|
||||
const params = (lat && lon)
|
||||
? `lat=${lat}&lon=${lon}&appid=${apiKey}&units=${this.units}`
|
||||
: `q=${city}&appid=${apiKey}&units=${this.units}`;
|
||||
return `${widgetApiEndpoints.weather}?${params}`;
|
||||
const { city, cityId, lat, lon } = this.options;
|
||||
let locationParams;
|
||||
if (lat && lon) {
|
||||
locationParams = `lat=${lat}&lon=${lon}`;
|
||||
} else if (cityId) {
|
||||
locationParams = `id=${cityId}`;
|
||||
} else {
|
||||
locationParams = `q=${city}`;
|
||||
}
|
||||
return `${widgetApiEndpoints.weather}?${locationParams}&appid=${apiKey}&units=${this.units}`;
|
||||
},
|
||||
tempDisplayUnits() {
|
||||
switch (this.units) {
|
||||
@@ -111,8 +116,8 @@ export default {
|
||||
const ops = this.options;
|
||||
if (!ops.apiKey) this.error('Missing API key for OpenWeatherMap');
|
||||
|
||||
if ((!ops.lat || !ops.lon) && !ops.city) {
|
||||
this.error('A city name or lat + lon is required to fetch weather');
|
||||
if ((!ops.lat || !ops.lon) && !ops.city && !ops.cityId) {
|
||||
this.error('A city name, city ID or lat + lon is required to fetch weather');
|
||||
}
|
||||
|
||||
if (ops.units && ops.units !== 'metric' && ops.units !== 'imperial') {
|
||||
|
||||
@@ -57,9 +57,17 @@ export default {
|
||||
},
|
||||
endpoint() {
|
||||
const apiKey = this.parseAsEnvVar(this.options.apiKey);
|
||||
const { city } = this.options;
|
||||
const { city, cityId, lat, lon } = this.options;
|
||||
const cnt = this.numDays * 8; // API returns 3-hourly entries; request enough to cover numDays
|
||||
const params = `?q=${city}&cnt=${cnt}&units=${this.units}&appid=${apiKey}`;
|
||||
let locationParams;
|
||||
if (lat && lon) {
|
||||
locationParams = `lat=${lat}&lon=${lon}`;
|
||||
} else if (cityId) {
|
||||
locationParams = `id=${cityId}`;
|
||||
} else {
|
||||
locationParams = `q=${city}`;
|
||||
}
|
||||
const params = `?${locationParams}&cnt=${cnt}&units=${this.units}&appid=${apiKey}`;
|
||||
return `${widgetApiEndpoints.weatherForecast}${params}`;
|
||||
},
|
||||
tempDisplayUnits() {
|
||||
@@ -149,7 +157,9 @@ export default {
|
||||
checkProps() {
|
||||
const ops = this.options;
|
||||
if (!ops.apiKey) this.error('Missing API key for OpenWeatherMap');
|
||||
if (!ops.city) this.error('A city name is required to fetch weather');
|
||||
if (!ops.city && !ops.cityId && !(ops.lat && ops.lon)) {
|
||||
this.error('A city name, city ID or lat + lon is required to fetch weather');
|
||||
}
|
||||
if (ops.units && ops.units !== 'metric' && ops.units !== 'imperial') {
|
||||
this.error('Invalid units specified, must be either \'metric\' or \'imperial\'');
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ const mount = () => app.mount('#app');
|
||||
/* Handle failures of third-party auth initialization */
|
||||
const handleAuthFailure = (provider, err) => {
|
||||
ErrorHandler(`Failed to authenticate with ${provider}`, err);
|
||||
store.commit(Keys.CRITICAL_ERROR_MSG, `Authentication failed (${provider}). See console for details.`);
|
||||
store.commit(Keys.CRITICAL_ERROR_MSG, `Authentication failed (${provider}).`);
|
||||
router.replace({ name: 'login' }).catch(() => {}).finally(mount);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,15 +18,21 @@ import Keys from '@/utils/StoreMutations';
|
||||
import { isAuthEnabled, isLoggedIn, isGuestAccessEnabled } from '@/utils/auth/Auth';
|
||||
import { isOidcEnabled } from '@/utils/auth/OidcAuth';
|
||||
import { isKeycloakEnabled } from '@/utils/auth/KeycloakAuth';
|
||||
import { isHeaderAuthEnabled } from '@/utils/auth/HeaderAuth';
|
||||
import { startingView as defaultStartingView, routePaths } from '@/utils/config/defaults';
|
||||
import { VIEW_META } from '@/utils/config/ConfigHelpers';
|
||||
import ErrorHandler from '@/utils/logging/ErrorHandler';
|
||||
|
||||
const progress = new Progress({ color: 'var(--progress-bar)' });
|
||||
|
||||
/* True for ANY auth (OIDC, KC, HeaderAuth, etc) */
|
||||
const isAnyAuthConfigured = () =>
|
||||
isAuthEnabled() || isOidcEnabled() || isKeycloakEnabled() || isHeaderAuthEnabled();
|
||||
|
||||
/* Returns true if user is already authenticated, or if auth is not enabled */
|
||||
const isAuthenticated = () => {
|
||||
const authEnabled = isAuthEnabled();
|
||||
if (store.state.criticalError) return false;
|
||||
const authEnabled = isAnyAuthConfigured();
|
||||
const userLoggedIn = isLoggedIn();
|
||||
const guestEnabled = isGuestAccessEnabled();
|
||||
return (!authEnabled || userLoggedIn || guestEnabled);
|
||||
|
||||
@@ -33,14 +33,11 @@ const getUsers = () => {
|
||||
// Otherwise, return the users array, if available
|
||||
|
||||
const users = auth.users ? [...auth.users] : [];
|
||||
if (isOidcEnabled()) {
|
||||
if (localStorage[localStorageKeys.USERNAME]) {
|
||||
const user = {
|
||||
user: localStorage[localStorageKeys.USERNAME],
|
||||
type: localStorage[localStorageKeys.ISADMIN] === 'true' ? 'admin' : 'normal',
|
||||
};
|
||||
users.push(user);
|
||||
}
|
||||
if ((isOidcEnabled() || isKeycloakEnabled()) && localStorage[localStorageKeys.USERNAME]) {
|
||||
users.push({
|
||||
user: localStorage[localStorageKeys.USERNAME],
|
||||
type: localStorage[localStorageKeys.ISADMIN] === 'true' ? 'admin' : 'normal',
|
||||
});
|
||||
}
|
||||
|
||||
return users;
|
||||
@@ -91,7 +88,7 @@ export const isLoggedIn = () => {
|
||||
const users = getUsers();
|
||||
const cookieToken = getCookieToken();
|
||||
|
||||
if (isOidcEnabled()) {
|
||||
if (isOidcEnabled() || isKeycloakEnabled()) {
|
||||
const username = localStorage[localStorageKeys.USERNAME]; // Get username
|
||||
if (!username) return false; // No username
|
||||
return users.some((user) => (
|
||||
|
||||
@@ -17,8 +17,10 @@ class KeycloakAuth {
|
||||
constructor(Keycloak) {
|
||||
const { auth } = getAppConfig();
|
||||
const {
|
||||
serverUrl, realm, clientId, idpHint, legacySupport,
|
||||
serverUrl, realm, clientId, idpHint, legacySupport, adminGroup, adminRole,
|
||||
} = auth.keycloak;
|
||||
this.adminGroup = adminGroup;
|
||||
this.adminRole = adminRole;
|
||||
if (typeof clientId === 'number' && !Number.isSafeInteger(clientId)) {
|
||||
ErrorHandler(
|
||||
'Keycloak clientId appears invalid. ',
|
||||
@@ -31,12 +33,12 @@ class KeycloakAuth {
|
||||
const loginOptions = idpHint ? { idpHint } : {};
|
||||
|
||||
this.loginOptions = loginOptions;
|
||||
this.keycloakClient = Keycloak(initOptions);
|
||||
this.keycloakClient = new Keycloak(initOptions);
|
||||
}
|
||||
|
||||
login() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.keycloakClient.init({ onLoad: 'check-sso' })
|
||||
this.keycloakClient.init({ onLoad: 'check-sso', responseMode: 'query' })
|
||||
.then((auth) => {
|
||||
if (auth) {
|
||||
this.storeKeycloakInfo();
|
||||
@@ -55,6 +57,8 @@ class KeycloakAuth {
|
||||
logout() {
|
||||
localStorage.removeItem(localStorageKeys.USERNAME);
|
||||
localStorage.removeItem(localStorageKeys.KEYCLOAK_INFO);
|
||||
localStorage.removeItem(localStorageKeys.ISADMIN);
|
||||
localStorage.removeItem(localStorageKeys.ID_TOKEN);
|
||||
this.keycloakClient.logout();
|
||||
}
|
||||
|
||||
@@ -68,10 +72,10 @@ class KeycloakAuth {
|
||||
preferred_username: preferredUsername,
|
||||
} = this.keycloakClient.tokenParsed;
|
||||
|
||||
const realmRoles = realmAccess.roles || [];
|
||||
const realmRoles = (realmAccess && realmAccess.roles) || [];
|
||||
|
||||
let clientRoles = [];
|
||||
if (Object.hasOwn(resourceAccess, clientId)) {
|
||||
if (resourceAccess && Object.hasOwn(resourceAccess, clientId)) {
|
||||
clientRoles = resourceAccess[clientId].roles || [];
|
||||
}
|
||||
|
||||
@@ -82,8 +86,18 @@ class KeycloakAuth {
|
||||
roles,
|
||||
};
|
||||
|
||||
// Compute isAdmin from the configured admin group/role
|
||||
const isAdmin = (Array.isArray(groups) && this.adminGroup && groups.includes(this.adminGroup))
|
||||
|| (Array.isArray(roles) && this.adminRole && roles.includes(this.adminRole))
|
||||
|| false;
|
||||
|
||||
localStorage.setItem(localStorageKeys.KEYCLOAK_INFO, JSON.stringify(info));
|
||||
localStorage.setItem(localStorageKeys.USERNAME, preferredUsername);
|
||||
localStorage.setItem(localStorageKeys.ISADMIN, isAdmin);
|
||||
// Save id_token locally, so it can be attached as Bearer for network requests
|
||||
if (this.keycloakClient.idToken) {
|
||||
localStorage.setItem(localStorageKeys.ID_TOKEN, this.keycloakClient.idToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { localStorageKeys } from '@/utils/config/defaults';
|
||||
import ErrorHandler from '@/utils/logging/ErrorHandler';
|
||||
import { statusMsg, statusErrorMsg } from '@/utils/logging/CoolConsole';
|
||||
|
||||
// Session storage config for storing last sign-in attempt
|
||||
const SIGNIN_GUARD_KEY = 'dashy.oidc.signin-attempt';
|
||||
const SIGNIN_GUARD_THRESHOLD_MS = 5 * 1000;
|
||||
|
||||
const getAppConfig = () => {
|
||||
const Accumulator = new ConfigAccumulator();
|
||||
const config = Accumulator.config();
|
||||
@@ -46,46 +50,87 @@ class OidcAuth {
|
||||
this.adminGroup = adminGroup;
|
||||
this.adminRole = adminRole;
|
||||
this.userManager = new UserManager(settings);
|
||||
|
||||
// Surface OIDC errors that fire outside the init promise chain
|
||||
this.userManager.events.addSilentRenewError((err) => {
|
||||
ErrorHandler('OIDC silent token renew failed', err);
|
||||
});
|
||||
this.userManager.events.addUserSignedOut(() => {
|
||||
statusMsg('OIDC', 'User signed out at provider');
|
||||
});
|
||||
}
|
||||
|
||||
async login() {
|
||||
const url = new URL(window.location.href);
|
||||
const code = url.searchParams.get('code');
|
||||
const providerError = url.searchParams.get('error');
|
||||
|
||||
// Provider redirected back with an error
|
||||
if (providerError && !code) {
|
||||
const desc = url.searchParams.get('error_description') || '';
|
||||
throw new Error(`OIDC provider returned ${providerError}: ${desc}`);
|
||||
}
|
||||
|
||||
if (code) {
|
||||
await this.userManager.signinCallback(window.location.href);
|
||||
// Populate localStorage before the reload so the post-reload route guard
|
||||
// sees the user as logged-in and lets them through to /, not /login.
|
||||
const callbackUser = await this.userManager.signinCallback(window.location.href);
|
||||
if (!callbackUser) {
|
||||
throw new Error(
|
||||
'OIDC signinCallback returned no user. Check userinfo CORS, '
|
||||
+ 'requested scopes, and that id_token claims include a username.',
|
||||
);
|
||||
}
|
||||
this.persistUserInfo(callbackUser);
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.userManager.getUser();
|
||||
|
||||
if (user === null) {
|
||||
if (!isOidcGuestAccessEnabled()) {
|
||||
// Bail with error, if we've literally just redirected. Prevents loop
|
||||
const lastAttempt = Number(sessionStorage.getItem(SIGNIN_GUARD_KEY)) || 0;
|
||||
if (Date.now() - lastAttempt < SIGNIN_GUARD_THRESHOLD_MS) {
|
||||
sessionStorage.removeItem(SIGNIN_GUARD_KEY);
|
||||
throw new Error(
|
||||
'OIDC sign-in redirect loop detected. Check provider redirect URIs '
|
||||
+ 'and that id_token claims include a username.',
|
||||
);
|
||||
}
|
||||
sessionStorage.setItem(SIGNIN_GUARD_KEY, String(Date.now()));
|
||||
await this.userManager.signinRedirect();
|
||||
}
|
||||
} else {
|
||||
const { roles = [], groups = [] } = user.profile;
|
||||
const info = {
|
||||
groups,
|
||||
roles,
|
||||
};
|
||||
const isAdmin = (Array.isArray(groups) && groups.includes(this.adminGroup))
|
||||
|| (Array.isArray(roles) && roles.includes(this.adminRole))
|
||||
|| false;
|
||||
|
||||
statusMsg(`user: ${user.profile.preferred_username} admin: ${isAdmin}`, JSON.stringify(info));
|
||||
|
||||
localStorage.setItem(localStorageKeys.KEYCLOAK_INFO, JSON.stringify(info));
|
||||
localStorage.setItem(localStorageKeys.USERNAME, user.profile.preferred_username);
|
||||
localStorage.setItem(localStorageKeys.ISADMIN, isAdmin);
|
||||
this.persistUserInfo(user);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mirror the OIDC user into the localStorage keys other parts of Dashy read */
|
||||
persistUserInfo(user) {
|
||||
const { roles = [], groups = [] } = user.profile;
|
||||
const info = { groups, roles };
|
||||
const isAdmin = (Array.isArray(groups) && groups.includes(this.adminGroup))
|
||||
|| (Array.isArray(roles) && roles.includes(this.adminRole))
|
||||
|| false;
|
||||
// Fall back through username candidates so USERNAME is always a non-empty
|
||||
const username = user.profile.preferred_username
|
||||
|| user.profile.email
|
||||
|| user.profile.sub
|
||||
|| 'oidc-user';
|
||||
statusMsg(`Authenticated as ${username} ${isAdmin ? '(admin)' : '(non-admin)'}`, JSON.stringify(info));
|
||||
localStorage.setItem(localStorageKeys.KEYCLOAK_INFO, JSON.stringify(info));
|
||||
localStorage.setItem(localStorageKeys.USERNAME, username);
|
||||
localStorage.setItem(localStorageKeys.ISADMIN, isAdmin);
|
||||
if (user.id_token) localStorage.setItem(localStorageKeys.ID_TOKEN, user.id_token);
|
||||
sessionStorage.removeItem(SIGNIN_GUARD_KEY);
|
||||
}
|
||||
|
||||
async logout() {
|
||||
localStorage.removeItem(localStorageKeys.USERNAME);
|
||||
localStorage.removeItem(localStorageKeys.KEYCLOAK_INFO);
|
||||
localStorage.removeItem(localStorageKeys.ISADMIN);
|
||||
localStorage.removeItem(localStorageKeys.ID_TOKEN);
|
||||
|
||||
try {
|
||||
await this.userManager.signoutRedirect();
|
||||
@@ -105,7 +150,11 @@ export const isOidcEnabled = () => {
|
||||
let oidc;
|
||||
|
||||
export const initOidcAuth = async () => {
|
||||
const { UserManager, WebStorageStateStore } = await import('oidc-client-ts');
|
||||
const { UserManager, WebStorageStateStore, Log } = await import('oidc-client-ts');
|
||||
if (import.meta.env.DEV) {
|
||||
Log.setLogger(console);
|
||||
Log.setLevel(Log.INFO);
|
||||
}
|
||||
oidc = new OidcAuth(UserManager, WebStorageStateStore);
|
||||
return oidc.login();
|
||||
};
|
||||
|
||||
43
src/utils/auth/getApiAuthHeader.js
Normal file
43
src/utils/auth/getApiAuthHeader.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Returns Authorization header for internal API requests when OIDC/KC configured
|
||||
* Uses the id_token which is already stored locally after successful login
|
||||
*
|
||||
* Will return `null`, and cause the caller to fall through, when:
|
||||
* - no token has been stashed
|
||||
* - the stashed token can't be parsed
|
||||
* - the token has already expired
|
||||
*/
|
||||
|
||||
import { localStorageKeys } from '@/utils/config/defaults';
|
||||
|
||||
/* Base64URL → utf-8 string decode */
|
||||
function decodeBase64Url(input) {
|
||||
const padded = input.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const pad = padded.length % 4;
|
||||
return atob(pad ? padded + '='.repeat(4 - pad) : padded);
|
||||
}
|
||||
|
||||
/* Check JWT isn't expired, the server will handle the actual verification */
|
||||
function isExpired(token) {
|
||||
try {
|
||||
const [, payload] = token.split('.');
|
||||
if (!payload) return true;
|
||||
const claims = JSON.parse(decodeBase64Url(payload));
|
||||
if (typeof claims.exp !== 'number') return false;
|
||||
return claims.exp * 1000 <= Date.now();
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns { Authorization: 'Bearer …' } or null if no usable token is available */
|
||||
export default function getApiAuthHeader() {
|
||||
let token;
|
||||
try {
|
||||
token = localStorage.getItem(localStorageKeys.ID_TOKEN);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!token || isExpired(token)) return null;
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
@@ -694,6 +694,16 @@
|
||||
"type": "string",
|
||||
"description": "Set to the 'Alias' of an existing Identity Provider in the specified realm to skip the Keycloak login page and redirect straight to the external IdP for authentication"
|
||||
},
|
||||
"adminRole": {
|
||||
"title": "Admin Role",
|
||||
"type": "string",
|
||||
"description": "The realm or client role that grants admin privileges (mapped into realm_access.roles in the id_token). If not set, no roles will be considered admin."
|
||||
},
|
||||
"adminGroup": {
|
||||
"title": "Admin Group",
|
||||
"type": "string",
|
||||
"description": "The group claim value that grants admin privileges. Requires a Keycloak mapper that puts a 'groups' claim into the id_token. If not set, no groups will be considered admin."
|
||||
},
|
||||
"legacySupport": {
|
||||
"title": "Legacy Support",
|
||||
"type": "boolean",
|
||||
|
||||
@@ -145,6 +145,7 @@ const defaults = {
|
||||
LAST_USED: 'lastUsed',
|
||||
KEYCLOAK_INFO: 'keycloakInfo',
|
||||
ISADMIN: 'isAdmin',
|
||||
ID_TOKEN: 'idToken',
|
||||
DISABLE_CRITICAL_WARNING: 'disableCriticalWarning',
|
||||
},
|
||||
/* Key names for cookie identifiers */
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { makeBasicAuthHeaders } from '@/utils/auth/Auth';
|
||||
import getApiAuthHeader from '@/utils/auth/getApiAuthHeader';
|
||||
|
||||
/** Check if a request URL targets the local Dashy server */
|
||||
function isLocalRequest(url) {
|
||||
@@ -64,11 +65,17 @@ async function makeRequest(config) {
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// For local API requests, include basic auth headers when configured
|
||||
// For local API requests, attach auth headers when configured
|
||||
// Bearer (OIDC / Keycloak id_token) takes precedence over basic-auth cookie header
|
||||
if (isLocalRequest(fullUrl) && !fetchOptions.headers.Authorization) {
|
||||
const authConfig = makeBasicAuthHeaders();
|
||||
if (authConfig.headers) {
|
||||
Object.assign(fetchOptions.headers, authConfig.headers);
|
||||
const bearer = getApiAuthHeader();
|
||||
if (bearer) {
|
||||
Object.assign(fetchOptions.headers, bearer);
|
||||
} else {
|
||||
const authConfig = makeBasicAuthHeaders();
|
||||
if (authConfig.headers) {
|
||||
Object.assign(fetchOptions.headers, authConfig.headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -220,13 +220,12 @@ export default {
|
||||
}
|
||||
}
|
||||
&.orientation-horizontal, &.orientation-vertical, &.single-section-view {
|
||||
@include phone { --content-max-width: 100%; }
|
||||
@include tablet { --content-max-width: 98%; }
|
||||
@include laptop { --content-max-width: 90%; }
|
||||
@include monitor { --content-max-width: 85%; }
|
||||
@include big-screen { --content-max-width: 80%; }
|
||||
@include big-screen-up { --content-max-width: 60%; }
|
||||
max-width: var(--content-max-width, 90%);
|
||||
@include phone { max-width: var(--content-max-width, 100%); }
|
||||
@include tablet { max-width: var(--content-max-width, 98%); }
|
||||
@include laptop { max-width: var(--content-max-width, 90%); }
|
||||
@include monitor { max-width: var(--content-max-width, 85%); }
|
||||
@include big-screen { max-width: var(--content-max-width, 80%); }
|
||||
@include big-screen-up { max-width: var(--content-max-width, 60%); }
|
||||
}
|
||||
|
||||
/* Specify number of columns, based on screen size or user preference */
|
||||
|
||||
264
tests/locales/check-locales.js
Normal file
264
tests/locales/check-locales.js
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Dashy's Internationalization lint and test script
|
||||
*
|
||||
* Scans code (in src) for translation calls ($t, $tc, i18n.t, i18n.global.t)
|
||||
* and then compares against the translation content (in src/assets/locales)
|
||||
* en.json is the most important, as missing locales can fallback to English
|
||||
*
|
||||
* Checks:
|
||||
* - All locale files are present, valid and parsable (failure)
|
||||
* - Locales registered in src/utils/languages.js but with no JSON file (failure)
|
||||
* - Locale JSON files not registered in src/utils/languages.js (failure)
|
||||
* - Non-existing translations used in the code, not present in en.json (failure)
|
||||
* - Translations in en.json never used anywhere in the code (warn)
|
||||
* - Translations in other locales not used in en.json or code (warn)
|
||||
* - Missing translations in other locales compared to en.json (coverage report)
|
||||
*
|
||||
* Run `yarn validate-locales` to use. Also runs as part of the CI workflow on PRs
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const SRC = path.join(ROOT, 'src');
|
||||
const LOCALES_DIR = path.join(SRC, 'assets', 'locales');
|
||||
const LANGUAGES_FILE = path.join(SRC, 'utils', 'languages.js');
|
||||
const SOURCE_LOCALE = 'en';
|
||||
|
||||
const c = {
|
||||
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
||||
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m', cyan: '\x1b[36m', grey: '\x1b[90m',
|
||||
};
|
||||
const paint = (col, s) => `${c[col]}${s}${c.reset}`;
|
||||
const coverageColour = (pct) => (pct === 100 ? 'green' : pct >= 80 ? 'cyan' : pct >= 50 ? 'yellow' : 'red');
|
||||
const hdr = (t) => `\n${paint('bold', t)}\n${paint('grey', '─'.repeat(stripAnsi(t).length))}`;
|
||||
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
|
||||
/* Keys used indirectly (computed at runtime) that the static scanner can't see */
|
||||
const IGNORED_KEYS = new Set([
|
||||
// src/components/Configuration/JsonEditor.vue
|
||||
'config-editor.status-warning',
|
||||
'config-editor.status-warnings',
|
||||
// src/components/InteractiveEditor/ExportConfigMenu.vue
|
||||
'interactive-editor.export.status-error',
|
||||
'interactive-editor.export.status-loading',
|
||||
'interactive-editor.export.status-unknown',
|
||||
'interactive-editor.export.status-valid',
|
||||
// src/components/Settings/AuthButtons.vue
|
||||
'settings.sign-in-tooltip',
|
||||
'settings.sign-out-tooltip',
|
||||
// src/utils/InitServiceWorker.js
|
||||
'updates.sw-update-action',
|
||||
'updates.sw-update-available',
|
||||
]);
|
||||
|
||||
/* Walk a dir, returning files matching any of the given extensions. */
|
||||
const walk = (dir, exts, out = []) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
const p = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(p, exts, out);
|
||||
else if (exts.some((e) => p.endsWith(e))) out.push(p);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/* Flatten a nested locale object into { 'a.b.c': true } form. */
|
||||
const flatten = (obj, prefix = '', out = {}) => {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
const key = prefix ? `${prefix}.${k}` : k;
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) flatten(v, key, out);
|
||||
else out[key] = true;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/* Extract registered locale codes from src/utils/languages.js. */
|
||||
const loadRegisteredCodes = () => {
|
||||
const src = fs.readFileSync(LANGUAGES_FILE, 'utf8');
|
||||
return [...src.matchAll(/code:\s*(['"])([^'"]+)\1/g)].map((m) => m[2]);
|
||||
};
|
||||
|
||||
/* Parse + sanity-check a locale file. Returns { parsed } or { error }. */
|
||||
const loadLocale = (file) => {
|
||||
const rel = path.relative(ROOT, file);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
} catch (e) {
|
||||
return { error: `${rel}: invalid JSON — ${e.message}` };
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
const got = Array.isArray(parsed) ? 'array' : parsed === null ? 'null' : typeof parsed;
|
||||
return { error: `${rel}: root must be an object (got ${got})` };
|
||||
}
|
||||
return { parsed };
|
||||
};
|
||||
|
||||
// $t('key') / $tc("key") / `i18n.t('key')` / `this.$t('key')` / `i18n.global.t('key')`
|
||||
const RE_KEY = /(?:\$tc?|i18n(?:\.global)?\.tc?)\s*\(\s*(['"`])((?:(?!\1).)*?)\1/g;
|
||||
// Same callers, but with a variable as the first arg (no quote).
|
||||
const RE_DYNAMIC = /(?:\$tc?|i18n(?:\.global)?\.tc?)\s*\(\s*([a-zA-Z_$][\w$.]*)\s*[,)]/g;
|
||||
|
||||
const extractFromFile = (file) => {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
const literals = new Set();
|
||||
const prefixes = new Set();
|
||||
const dynamic = [];
|
||||
for (const m of src.matchAll(RE_KEY)) {
|
||||
const [, quote, value] = m;
|
||||
// Backtick with `${...}` = dynamic key with static prefix.
|
||||
if (quote === '`' && value.includes('${')) {
|
||||
const pfx = value.split('${')[0].replace(/\.$/, '');
|
||||
if (pfx) prefixes.add(pfx);
|
||||
} else {
|
||||
literals.add(value);
|
||||
}
|
||||
}
|
||||
for (const m of src.matchAll(RE_DYNAMIC)) {
|
||||
const line = src.slice(0, m.index).split('\n').length;
|
||||
dynamic.push({ file: path.relative(ROOT, file), line, variable: m[1] });
|
||||
}
|
||||
return { literals, prefixes, dynamic };
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
// Load + validate locales (fail fast on bad JSON)
|
||||
const localeFiles = fs.readdirSync(LOCALES_DIR).filter((f) => f.endsWith('.json'));
|
||||
const locales = {};
|
||||
const localeErrors = [];
|
||||
for (const f of localeFiles) {
|
||||
const result = loadLocale(path.join(LOCALES_DIR, f));
|
||||
if (result.error) localeErrors.push(result.error);
|
||||
else locales[f.replace(/\.json$/, '')] = flatten(result.parsed);
|
||||
}
|
||||
if (localeErrors.length) {
|
||||
console.error(hdr(paint('red', `FAIL: ${localeErrors.length} invalid locale file(s)`)));
|
||||
for (const e of localeErrors) console.error(` ${paint('red', '✗')} ${e}`);
|
||||
console.error(paint('red', '\n✗ FAIL\n'));
|
||||
process.exit(2);
|
||||
}
|
||||
const en = locales[SOURCE_LOCALE];
|
||||
if (!en) {
|
||||
console.error(paint('red', `Source locale ${SOURCE_LOCALE}.json not found`));
|
||||
process.exit(2);
|
||||
}
|
||||
const enKeys = Object.keys(en);
|
||||
|
||||
// Scan source
|
||||
const files = walk(SRC, ['.vue', '.js'])
|
||||
.filter((f) => !f.startsWith(LOCALES_DIR + path.sep));
|
||||
|
||||
const usedLiterals = new Set();
|
||||
const usedPrefixes = new Set();
|
||||
const dynamicCalls = [];
|
||||
for (const f of files) {
|
||||
const r = extractFromFile(f);
|
||||
r.literals.forEach((k) => usedLiterals.add(k));
|
||||
r.prefixes.forEach((p) => usedPrefixes.add(p));
|
||||
dynamicCalls.push(...r.dynamic);
|
||||
}
|
||||
|
||||
// Cross-check locale files against the registry in languages.js
|
||||
const registered = new Set(loadRegisteredCodes());
|
||||
const fileCodes = new Set(Object.keys(locales));
|
||||
const missingFiles = [...registered].filter((c) => !fileCodes.has(c)).sort();
|
||||
const unregistered = [...fileCodes].filter((c) => !registered.has(c)).sort();
|
||||
|
||||
// A key is "potentially used" if a literal matches OR a dynamic prefix covers it.
|
||||
const matchesPrefix = (k) => [...usedPrefixes].some((p) => k === p || k.startsWith(`${p}.`));
|
||||
const isUsed = (k) => usedLiterals.has(k) || matchesPrefix(k);
|
||||
|
||||
// Diff
|
||||
const missingInEn = [...usedLiterals].filter((k) => !(k in en)).sort();
|
||||
const unusedInEn = enKeys.filter((k) => !isUsed(k) && !IGNORED_KEYS.has(k)).sort();
|
||||
const extras = {};
|
||||
const coverage = {};
|
||||
for (const [code, keys] of Object.entries(locales)) {
|
||||
if (code === SOURCE_LOCALE) continue;
|
||||
extras[code] = Object.keys(keys).filter((k) => !(k in en)).sort();
|
||||
const have = enKeys.filter((k) => k in keys).length;
|
||||
coverage[code] = (have / enKeys.length) * 100;
|
||||
}
|
||||
|
||||
// Printy print
|
||||
console.log(hdr('Locale check'));
|
||||
console.log(` ${paint('dim', 'Source files scanned')} ${files.length}`);
|
||||
console.log(` ${paint('dim', 'Literal keys used ')} ${usedLiterals.size}`);
|
||||
console.log(` ${paint('dim', 'Dynamic prefixes ')} ${usedPrefixes.size}`);
|
||||
console.log(` ${paint('dim', 'Dynamic call sites ')} ${dynamicCalls.length}`);
|
||||
console.log(` ${paint('dim', 'Locales found ')} ${Object.keys(locales).length}`);
|
||||
|
||||
if (missingFiles.length) {
|
||||
console.log(hdr(paint('red', `FAIL: ${missingFiles.length} locale(s) registered in languages.js but no JSON file`)));
|
||||
for (const c of missingFiles) console.log(` ${paint('red', '✗')} ${c}`);
|
||||
}
|
||||
|
||||
if (unregistered.length) {
|
||||
console.log(hdr(paint('red', `FAIL: ${unregistered.length} locale file(s) not registered in languages.js`)));
|
||||
for (const c of unregistered) console.log(` ${paint('red', '✗')} ${c}`);
|
||||
}
|
||||
|
||||
if (missingInEn.length) {
|
||||
console.log(hdr(paint('red', `FAIL: ${missingInEn.length} key(s) used in code but missing from ${SOURCE_LOCALE}.json`)));
|
||||
for (const k of missingInEn) console.log(` ${paint('red', '✗')} ${k}`);
|
||||
}
|
||||
|
||||
if (unusedInEn.length) {
|
||||
console.log(hdr(paint('yellow', `WARN: ${unusedInEn.length} key(s) in ${SOURCE_LOCALE}.json not used in code`)));
|
||||
for (const k of unusedInEn) console.log(` ${paint('yellow', '·')} ${k}`);
|
||||
}
|
||||
|
||||
const extraEntries = Object.entries(extras).filter(([, v]) => v.length);
|
||||
if (extraEntries.length) {
|
||||
const total = extraEntries.reduce((n, [, v]) => n + v.length, 0);
|
||||
console.log(hdr(paint('yellow', `WARN: ${total} key(s) in other locales but not in ${SOURCE_LOCALE}.json`)));
|
||||
for (const [code, ks] of extraEntries) {
|
||||
console.log(` ${paint('cyan', code)} ${paint('grey', `(${ks.length})`)}`);
|
||||
for (const k of ks) console.log(` ${paint('yellow', '·')} ${k}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(hdr('Coverage vs en.json'));
|
||||
const sorted = Object.entries(coverage).sort(([a], [b]) => a.localeCompare(b));
|
||||
for (const [code, pct] of sorted) {
|
||||
const filled = Math.round(pct / 5);
|
||||
const bar = '█'.repeat(filled) + paint('grey', '░'.repeat(20 - filled));
|
||||
const col = coverageColour(pct);
|
||||
console.log(` ${code.padEnd(11)} ${paint(col, bar)} ${paint(col, `${pct.toFixed(1)}%`)}`);
|
||||
}
|
||||
|
||||
if (dynamicCalls.length) {
|
||||
console.log(hdr(paint('grey', `Note: ${dynamicCalls.length} dynamic $t() call(s) — keys under these can't be verified`)));
|
||||
for (const d of dynamicCalls) {
|
||||
console.log(` ${paint('grey', `${d.file}:${d.line}`)} → ${paint('dim', d.variable)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(hdr('Summary'));
|
||||
const fail = missingInEn.length > 0 || missingFiles.length > 0 || unregistered.length > 0;
|
||||
const extraCount = extraEntries.reduce((n, [, v]) => n + v.length, 0);
|
||||
const coverageVals = Object.values(coverage);
|
||||
const avgCoverage = coverageVals.length
|
||||
? coverageVals.reduce((a, b) => a + b, 0) / coverageVals.length
|
||||
: 0;
|
||||
const row = (label, value, col) => console.log(`▶ ${label.padEnd(16)} ${paint(col, value)}`);
|
||||
const validLocales = Object.keys(locales).length;
|
||||
const totalLocales = localeFiles.length;
|
||||
row('Valid Locales', `${validLocales}/${totalLocales}`, totalLocales === validLocales ? 'green' : 'blue');
|
||||
row('Total keys', `${enKeys.length}`, 'blue');
|
||||
row('Avg coverage', `${avgCoverage.toFixed(1)}%`, coverageColour(avgCoverage));
|
||||
row('Missing', `${missingInEn.length}`, missingInEn.length ? 'red' : 'green');
|
||||
row('Unused', `${unusedInEn.length}`, unusedInEn.length ? 'yellow' : 'green');
|
||||
row('Extras', `${extraCount}`, extraCount ? 'yellow' : 'green');
|
||||
row('Missing files', `${missingFiles.length}`, missingFiles.length ? 'red' : 'green');
|
||||
row('Unregistered', `${unregistered.length}`, unregistered.length ? 'red' : 'green');
|
||||
console.log(fail ? paint('red', '\n✗ FAIL\n') : paint('green', '\n✓ PASS\n'));
|
||||
process.exit(fail ? 1 : 0);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -1,10 +1,18 @@
|
||||
// @vitest-environment node
|
||||
import http from 'http';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import {
|
||||
describe, it, expect, afterEach, beforeAll, afterAll, vi,
|
||||
} from 'vitest';
|
||||
import request from 'supertest';
|
||||
|
||||
// Isolate from the repo's conf.yml so test behaviour doesn't depend on which
|
||||
// auth method (if any) the developer has configured locally.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashy-cors-test-'));
|
||||
process.env.USER_DATA_DIR = tmpDir;
|
||||
|
||||
const app = require('../../services/app');
|
||||
const { substituteEnv } = require('../../services/cors-proxy');
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
// @vitest-environment node
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import request from 'supertest';
|
||||
|
||||
// Isolate from the repo's conf.yml so test behaviour doesn't depend on which
|
||||
// auth method (if any) the developer has configured locally.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashy-general-test-'));
|
||||
fs.writeFileSync(path.join(tmpDir, 'conf.yml'), 'pageInfo:\n title: Test\nsections: []\n');
|
||||
process.env.USER_DATA_DIR = tmpDir;
|
||||
|
||||
const app = require('../../services/app');
|
||||
|
||||
describe('Healthcheck', () => {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// @vitest-environment node
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import request from 'supertest';
|
||||
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashy-status-test-'));
|
||||
process.env.USER_DATA_DIR = tmpDir;
|
||||
|
||||
const app = require('../../services/app');
|
||||
|
||||
describe('Status check', () => {
|
||||
|
||||
230
yarn.lock
230
yarn.lock
@@ -483,10 +483,10 @@
|
||||
"@babel/helper-module-transforms" "^7.28.6"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
|
||||
"@babel/plugin-transform-modules-systemjs@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz#e458a95a17807c415924106a3ff188a3b8dee964"
|
||||
integrity sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==
|
||||
"@babel/plugin-transform-modules-systemjs@^7.29.0", "@babel/plugin-transform-modules-systemjs@^7.29.4":
|
||||
version "7.29.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20"
|
||||
integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms" "^7.28.6"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
@@ -874,10 +874,10 @@
|
||||
dependencies:
|
||||
"@marijn/find-cluster-break" "^1.0.0"
|
||||
|
||||
"@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.37.0", "@codemirror/view@^6.41.1", "@codemirror/view@^6.42.0":
|
||||
version "6.42.1"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.42.1.tgz#2ce83206e0a7e1704b123ea3b1d1b32ad6abe3b0"
|
||||
integrity sha512-ToN3oFc0nsxNUYVF5P0ztLgbC4UPPjPtA9aKYhkOKQaZASpOUo6ISXyQLP66ctVwlDc+j6Jv0uK5IFALkiXztg==
|
||||
"@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.37.0", "@codemirror/view@^6.42.0", "@codemirror/view@^6.43.0":
|
||||
version "6.43.0"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.43.0.tgz#a577da65f1d5d8f7cbf08e14849284c12f38365a"
|
||||
integrity sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==
|
||||
dependencies:
|
||||
"@codemirror/state" "^6.6.0"
|
||||
crelt "^1.0.6"
|
||||
@@ -1057,10 +1057,10 @@
|
||||
debug "^4.3.1"
|
||||
minimatch "^10.2.4"
|
||||
|
||||
"@eslint/config-helpers@^0.5.5":
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.5.tgz#ae16134e4792ac5fbdc533548a24ac1ea9f7f3ae"
|
||||
integrity sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==
|
||||
"@eslint/config-helpers@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz#ef9a36881d39dfd5dbeac22b0da997fabfb08b03"
|
||||
integrity sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==
|
||||
dependencies:
|
||||
"@eslint/core" "^1.2.1"
|
||||
|
||||
@@ -1922,63 +1922,63 @@
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
|
||||
integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
|
||||
|
||||
"@vitest/expect@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.5.tgz#5caab19535cfb04fbc37087c5608d46e74dc9292"
|
||||
integrity sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==
|
||||
"@vitest/expect@4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.6.tgz#b50c9390aae6957ab4d9e20722cebb17d5bf169a"
|
||||
integrity sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==
|
||||
dependencies:
|
||||
"@standard-schema/spec" "^1.1.0"
|
||||
"@types/chai" "^5.2.2"
|
||||
"@vitest/spy" "4.1.5"
|
||||
"@vitest/utils" "4.1.5"
|
||||
"@vitest/spy" "4.1.6"
|
||||
"@vitest/utils" "4.1.6"
|
||||
chai "^6.2.2"
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
"@vitest/mocker@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.5.tgz#9d5791733e4866cfb8af2d48ca371b127e7d2e93"
|
||||
integrity sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==
|
||||
"@vitest/mocker@4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.6.tgz#6b624045745236b02aca879a02aef68b72d9d4cd"
|
||||
integrity sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==
|
||||
dependencies:
|
||||
"@vitest/spy" "4.1.5"
|
||||
"@vitest/spy" "4.1.6"
|
||||
estree-walker "^3.0.3"
|
||||
magic-string "^0.30.21"
|
||||
|
||||
"@vitest/pretty-format@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.5.tgz#4c13d77a77e2931e44db95522ed5700bcf0570d4"
|
||||
integrity sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==
|
||||
"@vitest/pretty-format@4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.6.tgz#24a1c03a6b68a8775f8ddfec51d3636315edc3f5"
|
||||
integrity sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==
|
||||
dependencies:
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
"@vitest/runner@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.5.tgz#a14dd2d2f48603f906dd52304a10c7fc623bb1de"
|
||||
integrity sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==
|
||||
"@vitest/runner@4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.6.tgz#b6d189e68bd9927c4f111ad089ff96e4757591b1"
|
||||
integrity sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==
|
||||
dependencies:
|
||||
"@vitest/utils" "4.1.5"
|
||||
"@vitest/utils" "4.1.6"
|
||||
pathe "^2.0.3"
|
||||
|
||||
"@vitest/snapshot@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.5.tgz#d07970d1448190ee5a258db6ab79c65b8018c13b"
|
||||
integrity sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==
|
||||
"@vitest/snapshot@4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.6.tgz#14fdfc8baf6b4b3e4e35763431dbea3aaa8aa0eb"
|
||||
integrity sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "4.1.5"
|
||||
"@vitest/utils" "4.1.5"
|
||||
"@vitest/pretty-format" "4.1.6"
|
||||
"@vitest/utils" "4.1.6"
|
||||
magic-string "^0.30.21"
|
||||
pathe "^2.0.3"
|
||||
|
||||
"@vitest/spy@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.5.tgz#fa7858ffab746fa9ac29496e626f5a0caf9a5a7f"
|
||||
integrity sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==
|
||||
"@vitest/spy@4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.6.tgz#0a316893630f47fa545e33026cfc91575070d165"
|
||||
integrity sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==
|
||||
|
||||
"@vitest/ui@^4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-4.1.5.tgz#041ac70b0c769182a313574402e1bc15f0454d14"
|
||||
integrity sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==
|
||||
"@vitest/ui@^4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-4.1.6.tgz#9ab55522fcffcb6ebdc1607e8119dc6d08b68591"
|
||||
integrity sha512-wiu5em68DfGv/2HFvI1Njr7JI2CHcBlQvereSzVG8my53PRxjTNOCsD9VOkRKrsJBDHmyuXvosxWZw7T91a2mw==
|
||||
dependencies:
|
||||
"@vitest/utils" "4.1.5"
|
||||
"@vitest/utils" "4.1.6"
|
||||
fflate "^0.8.2"
|
||||
flatted "^3.4.2"
|
||||
pathe "^2.0.3"
|
||||
@@ -1986,12 +1986,12 @@
|
||||
tinyglobby "^0.2.15"
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
"@vitest/utils@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.5.tgz#20d6a6ae651a0dd33f945548921698d49701fa43"
|
||||
integrity sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==
|
||||
"@vitest/utils@4.1.6":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.6.tgz#3f4acf1f60e135ec1ce896f10baa4cd6466d0d38"
|
||||
integrity sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "4.1.5"
|
||||
"@vitest/pretty-format" "4.1.6"
|
||||
convert-source-map "^2.0.0"
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
@@ -2063,15 +2063,15 @@
|
||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||
|
||||
"@vue/language-core@3.2.8":
|
||||
version "3.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-3.2.8.tgz#3bd38c343b89976208b7996bc670df56313047de"
|
||||
integrity sha512-9OiSPQFiAAWNVnXb0d2dcTmcKnFQamhuNES6ayyISrb/mwPWVgoGdAqSfCWqKhQpa3D5gDTcYD+w7ObiheZ81g==
|
||||
"@vue/language-core@3.2.9":
|
||||
version "3.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-3.2.9.tgz#903dde38a614f6263ace162c9edc2cd479e6c69b"
|
||||
integrity sha512-ie0ojt/0fU/GfIogh+zgHbaYRPlt9S+cLOxcWwF7nTSFh897BVgnFKL2byT4kpp1mlqYWZ2psGwSniyE2xsxYw==
|
||||
dependencies:
|
||||
"@volar/language-core" "2.4.28"
|
||||
"@vue/compiler-dom" "^3.5.0"
|
||||
"@vue/shared" "^3.5.0"
|
||||
alien-signals "^3.1.2"
|
||||
alien-signals "^3.2.0"
|
||||
muggle-string "^0.4.1"
|
||||
path-browserify "^1.0.1"
|
||||
picomatch "^4.0.4"
|
||||
@@ -2179,10 +2179,10 @@ ajv@^8.0.0, ajv@^8.20.0, ajv@^8.6.0, ajv@^8.6.1:
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
alien-signals@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-3.1.2.tgz#26e623e3ed81e401df1a7c503f726e2288a4fa02"
|
||||
integrity sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==
|
||||
alien-signals@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-3.2.1.tgz#eb66256949bce90b7d30d055e2752e62d6930c7c"
|
||||
integrity sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
@@ -2331,10 +2331,10 @@ basic-auth@^2.0.1:
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
|
||||
body-parser@~1.20.3:
|
||||
version "1.20.4"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
|
||||
integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==
|
||||
body-parser@~1.20.5:
|
||||
version "1.20.5"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.5.tgz#303c8c34423d1d6fa799bc764e93c1e4dc6ebf64"
|
||||
integrity sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==
|
||||
dependencies:
|
||||
bytes "~3.1.2"
|
||||
content-type "~1.0.5"
|
||||
@@ -2344,7 +2344,7 @@ body-parser@~1.20.3:
|
||||
http-errors "~2.0.1"
|
||||
iconv-lite "~0.4.24"
|
||||
on-finished "~2.4.1"
|
||||
qs "~6.14.0"
|
||||
qs "~6.15.1"
|
||||
raw-body "~2.5.3"
|
||||
type-is "~1.6.18"
|
||||
unpipe "~1.0.0"
|
||||
@@ -2731,10 +2731,10 @@ domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@^3.4.1:
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.2.tgz#f0ff81be682c485505097ba8195a058d8f575218"
|
||||
integrity sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==
|
||||
dompurify@^3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.3.tgz#3ef336e7a757c3bf1efbd3781afb149a3ae5cfa4"
|
||||
integrity sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
@@ -3017,15 +3017,15 @@ eslint-visitor-keys@^3.4.3:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
|
||||
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
|
||||
|
||||
eslint@^10.2.1:
|
||||
version "10.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.3.0.tgz#ed5b810eb8e0191bf24bddcf9cdb45b974e0a16d"
|
||||
integrity sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==
|
||||
eslint@^10.4.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.0.tgz#d86b6c405de0f19f3318c47139b8cb6771b3f592"
|
||||
integrity sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@eslint/config-array" "^0.23.5"
|
||||
"@eslint/config-helpers" "^0.5.5"
|
||||
"@eslint/config-helpers" "^0.6.0"
|
||||
"@eslint/core" "^1.2.1"
|
||||
"@eslint/plugin-kit" "^0.7.1"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
@@ -3120,14 +3120,14 @@ express-basic-auth@^1.2.1:
|
||||
dependencies:
|
||||
basic-auth "^2.0.1"
|
||||
|
||||
express@^4.21.0:
|
||||
version "4.22.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069"
|
||||
integrity sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==
|
||||
express@^4.22.2:
|
||||
version "4.22.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700"
|
||||
integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "~1.20.3"
|
||||
body-parser "~1.20.5"
|
||||
content-disposition "~0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "~0.7.1"
|
||||
@@ -3146,7 +3146,7 @@ express@^4.21.0:
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "~0.1.12"
|
||||
proxy-addr "~2.0.7"
|
||||
qs "~6.14.0"
|
||||
qs "~6.15.1"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.2.1"
|
||||
send "~0.19.0"
|
||||
@@ -3178,9 +3178,9 @@ fast-safe-stringify@^2.1.1:
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fast-uri@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
|
||||
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
|
||||
integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
|
||||
|
||||
fdir@^6.4.4, fdir@^6.5.0:
|
||||
version "6.5.0"
|
||||
@@ -3803,6 +3803,11 @@ jake@^10.8.5:
|
||||
filelist "^1.0.4"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
jose@^5.10.0:
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-5.10.0.tgz#c37346a099d6467c401351a9a0c2161e0f52c4be"
|
||||
integrity sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==
|
||||
|
||||
js-beautify@^1.14.9:
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.4.tgz#f579f977ed4c930cef73af8f98f3f0a608acd51e"
|
||||
@@ -4273,10 +4278,10 @@ postcss-value-parser@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.4.31, postcss@^8.5.14, postcss@^8.5.3:
|
||||
version "8.5.13"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.13.tgz#6cfaf647f2e7ef69850208eccd849e0d3f65d420"
|
||||
integrity sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==
|
||||
postcss@^8.5.14, postcss@^8.5.3:
|
||||
version "8.5.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
|
||||
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
|
||||
dependencies:
|
||||
nanoid "^3.3.11"
|
||||
picocolors "^1.1.1"
|
||||
@@ -4315,20 +4320,13 @@ punycode@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
qs@^6.14.1:
|
||||
qs@^6.14.1, qs@~6.15.1:
|
||||
version "6.15.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.1.tgz#bdb55aed06bfac257a90c44a446a73fba5575c8f"
|
||||
integrity sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==
|
||||
dependencies:
|
||||
side-channel "^1.1.0"
|
||||
|
||||
qs@~6.14.0:
|
||||
version "6.14.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.2.tgz#b5634cf9d9ad9898e31fba3504e866e8efb6798c"
|
||||
integrity sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==
|
||||
dependencies:
|
||||
side-channel "^1.1.0"
|
||||
|
||||
range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
@@ -5234,18 +5232,18 @@ vite-svg-loader@^5.1.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vitest@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.5.tgz#cda189c0cd9dd1c920be477c0f371b64ec14782a"
|
||||
integrity sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==
|
||||
vitest@^4.1.6:
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.6.tgz#754875c9a09c5a3e8ca7d07d440659d92c19787f"
|
||||
integrity sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==
|
||||
dependencies:
|
||||
"@vitest/expect" "4.1.5"
|
||||
"@vitest/mocker" "4.1.5"
|
||||
"@vitest/pretty-format" "4.1.5"
|
||||
"@vitest/runner" "4.1.5"
|
||||
"@vitest/snapshot" "4.1.5"
|
||||
"@vitest/spy" "4.1.5"
|
||||
"@vitest/utils" "4.1.5"
|
||||
"@vitest/expect" "4.1.6"
|
||||
"@vitest/mocker" "4.1.6"
|
||||
"@vitest/pretty-format" "4.1.6"
|
||||
"@vitest/runner" "4.1.6"
|
||||
"@vitest/snapshot" "4.1.6"
|
||||
"@vitest/spy" "4.1.6"
|
||||
"@vitest/utils" "4.1.6"
|
||||
es-module-lexer "^2.0.0"
|
||||
expect-type "^1.3.0"
|
||||
magic-string "^0.30.21"
|
||||
@@ -5303,13 +5301,13 @@ vue-select@4.0.0-beta.6:
|
||||
resolved "https://registry.yarnpkg.com/vue-select/-/vue-select-4.0.0-beta.6.tgz#7c250cb7c01280b54a311cb446629801b3c8df98"
|
||||
integrity sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==
|
||||
|
||||
vue-tsc@^3.2.8:
|
||||
version "3.2.8"
|
||||
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-3.2.8.tgz#92e6190f198b460c92b35f4f66eb791e374f0c01"
|
||||
integrity sha512-27vTLJ6Q2370obOd0PFYoYoKnmXJ521uUIedrs3Zhhhg/8YG10VOCMmwt+JQslatpAMTDbnWiitLnoD5VlIvog==
|
||||
vue-tsc@^3.2.9:
|
||||
version "3.2.9"
|
||||
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-3.2.9.tgz#94998ec126c37eef1729daa62331713c30b52d95"
|
||||
integrity sha512-qm8/nbo+9eZc1SCndm9wT+gq23pM+wRIdHY0wjm83B3lIginHTwcdrLUyTrKjDWXbMVNjKegNrnymhpdqnCL3A==
|
||||
dependencies:
|
||||
"@volar/typescript" "2.4.28"
|
||||
"@vue/language-core" "3.2.8"
|
||||
"@vue/language-core" "3.2.9"
|
||||
|
||||
vue@^3.5.34:
|
||||
version "3.5.34"
|
||||
@@ -5622,10 +5620,10 @@ yallist@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
||||
yaml@^2.8.3:
|
||||
version "2.8.4"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.4.tgz#4b5f411dd25f9544914d8673d4da7f29248e5e2e"
|
||||
integrity sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==
|
||||
yaml@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4"
|
||||
integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
|
||||
Reference in New Issue
Block a user